merged master (2012-09-11)
authordaniel <daniel.kinzler@wikimedia.de>
Tue, 11 Sep 2012 09:43:02 +0000 (11:43 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Tue, 11 Sep 2012 09:43:02 +0000 (11:43 +0200)
Change-Id: I8e953eaa22f9d331b0af5e780fbeff6d702b23e3

21 files changed:
1  2 
docs/hooks.txt
includes/Article.php
includes/Content.php
includes/EditPage.php
includes/GlobalFunctions.php
includes/ImagePage.php
includes/LinksUpdate.php
includes/OutputPage.php
includes/Title.php
includes/WikiPage.php
includes/api/ApiMain.php
includes/filerepo/file/LocalFile.php
includes/installer/MysqlUpdater.php
includes/installer/SqliteUpdater.php
includes/search/SearchEngine.php
languages/messages/MessagesDe.php
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
maintenance/tables.sql
tests/phpunit/includes/WikitextContentTest.php
tests/phpunit/maintenance/backupTextPassTest.php

diff --combined docs/hooks.txt
@@@ -283,11 -283,6 +283,11 @@@ $article: Article objec
  $user: the User object that was created. (Parameter added in 1.7)
  $byEmail: true when account was created "by email" (added in 1.12)
  
 +'AfterFinalPageOutput': At the end of OutputPage::output() but before
 +final ob_end_flush() which will send the buffered output to the client.
 +This allows for last-minute modification of the output within the buffer
 +by using ob_get_clean().
 +
  'AfterImportPage': When a page import is completed
  $title: Title under which the revisions were imported
  $origTitle: Title provided by the XML file
@@@ -295,6 -290,12 +295,12 @@@ $revCount: Number of revisions in the X
  $sRevCount: Number of sucessfully imported revisions
  $pageInfo: associative array of page information
  
+ 'AfterFinalPageOutput': Nearly at the end of OutputPage::output() but
+ before OutputPage::sendCacheControl() and final ob_end_flush() which
+ will send the buffered output to the client. This allows for last-minute
+ modification of the output within the buffer by using ob_get_clean().
+ $output: The OutputPage object where output() was called
  'AjaxAddScript': Called in output page just before the initialisation
  of the javascript ajax engine. The hook is only called when ajax
  is enabled ( $wgUseAjax = true; ).
@@@ -319,6 -320,13 +325,13 @@@ $body: Body of the messag
  Use this to extend core API modules.
  &$module: Module object
  
+ 'APICheckCanExecute': Called during ApiMain::checkCanExecute. Use to
+ further authenticate and authorize API clients before executing the
+ module. Return false and set a message to cancel the request.
+ $module: Module object
+ $user: Current user
+ &$message: API usage message to die with
  'APIEditBeforeSave': before saving a page with api.php?action=edit,
  after processing request parameters. Return false to let the request
  fail, returning an error message or an <edit result="Failure"> tag
@@@ -420,14 -428,9 +433,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
@@@ -475,7 -478,7 +488,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
@@@ -483,18 -486,7 +496,18 @@@ $summary: Edit summary/commen
  $isMinor: Whether or not the edit was marked as minor
  $isWatch: (No longer used)
  $section: (No longer used)
 -$flags: Flags passed to Article::doEdit()
 +$flags: Flags passed to WikiPage::doEditContent()
 +$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 WikiPage::doEditContent()
  $revision: New Revision of the article
  
  'ArticleMergeComplete': after merging to article using Special:Mergehistory
@@@ -545,7 -537,7 +558,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
@@@ -554,16 -546,7 +567,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
@@@ -571,22 -554,9 +584,22 @@@ $summary: Edit summary/commen
  $isMinor: Whether or not the edit was marked as minor
  $isWatch: (No longer used)
  $section: (No longer used)
 -$flags: Flags passed to Article::doEdit()
 +$flags: Flags passed to WikiPage::doEditContent()
 +$revision: New Revision of the article
 +$status: Status object about to be returned by doEditContent()
 +$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 WikiPage::doEditContent()
  $revision: New Revision of the article
 -$status: Status object about to be returned by doEdit()
 +$status: Status object about to be returned by doEditContent()
  $baseRevId: the rev ID (or false) this edit was based on
  
  'ArticleUndelete': When one or more revisions of an article are restored
@@@ -615,19 -585,11 +628,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
@@@ -759,16 -721,6 +772,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
@@@ -842,19 -794,12 +855,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
@@@ -865,7 -810,7 +878,7 @@@ page
  $editPage: EditPage   object
  
  'EditPage::attemptSave': called before an article is
 -saved, that is before Article::doEdit() is called
 +saved, that is before WikiPage::doEditContent() is called
  $editpage_Obj: the current EditPage object
  
  'EditPage::importFormData': allow extensions to read additional data
@@@ -917,28 -862,14 +930,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"
 +
 +'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
 +'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
@@@ -1785,6 -1716,11 +1798,11 @@@ in the $searchEngine->namespaces array
  $query : Original query.
  &$parsed : Resultant query with the prefixes stripped.
  
+ 'SearchResultInitFromTitle': Set the revision used when displaying a page in
+ search results.
+ $title : Current Title object being displayed in search results.
+ &$id: Revision ID (default is false, for latest)
  'SearchableNamespaces': An option to modify which namespaces are searchable.
  &$arr : Array of namespaces ($nsId => $name) which will be used.
  
  '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
@@@ -2413,14 -2348,6 +2431,14 @@@ 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 Content's getDeletionUpdates() method.
 +$page: the WikiPage
 +$content: the Content to generate updates for
 +&$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 {
                                $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon';
 -                              $text = wfMessage( $message )->text();
 +                              $content = new MessageContent( $message, null, 'parsemag' );
                        }
                        wfProfileOut( __METHOD__ );
  
 -                      return $text;
 +                      return $content;
                } else {
 -                      $this->fetchContent();
 +                      $this->fetchContentObject();
                        wfProfileOut( __METHOD__ );
  
 -                      return $this->mContent;
 +                      return $this->mContentObject;
                }
        }
  
         * Get text of an article from database
         * Does *NOT* follow redirects.
         *
 +       * @protected
 +       * @note this is really internal functionality that should really NOT be used by other functions. For accessing
 +       *       article content, use the WikiPage class, especially WikiBase::getContent(). However, a lot of legacy code
 +       *       uses this method to retrieve page text from the database, so the function has to remain public for now.
 +       *
         * @return mixed string containing article contents, or false if null
 +       * @deprecated in 1.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!
 +              ContentHandler::runLegacyHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
 +
 +              wfProfileOut( __METHOD__ );
 +
 +              return $this->mContent;
 +      }
 +
 +
 +      /**
 +       * Get text content object
 +       * Does *NOT* follow redirects.
 +       * TODO: when is this null?
 +       *
 +       * @note code that wants to retrieve page content from the database should use WikiPage::getContent().
 +       *
 +       * @return Content|null
 +       *
 +       * @since 1.WD
 +       */
 +      protected function fetchContentObject() {
 +              if ( $this->mContentLoaded ) {
 +                      return $this->mContentObject;
 +              }
 +
 +              wfProfileIn( __METHOD__ );
 +
                $this->mContentLoaded = true;
 +              $this->mContent = null;
  
                $oldid = $this->getOldID();
  
                # Pre-fill content with error message so that if something
                # fails we'll have something telling us what we intended.
 -              $this->mContent = wfMessage( 'missing-revision', $oldid )->plain();
 +              // @todo: this isn't page content but a UI message. horrible.
 +              $this->mContentObject = new MessageContent( 'missing-revision', array( $oldid ), array() ) ;
  
                if ( $oldid ) {
                        # $this->mRevision might already be fetched by getOldIDFromRequest()
                        }
  
                        $this->mRevision = $this->mPage->getRevision();
 +
                        if ( !$this->mRevision ) {
                                wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" );
                                wfProfileOut( __METHOD__ );
  
                // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks.
                // We should instead work with the Revision object when we need it...
 -              $this->mContent = $this->mRevision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed
 +              $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER ); // Loads if user is allowed
                $this->mRevIdFetched = $this->mRevision->getId();
  
 -              wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
 +              wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) );
  
                wfProfileOut( __METHOD__ );
  
 -              return $this->mContent;
 +              return $this->mContentObject;
        }
  
        /**
         * @return Revision|null
         */
        public function getRevisionFetched() {
 -              $this->fetchContent();
 +              $this->fetchContentObject();
  
                return $this->mRevision;
        }
                                        break;
                                case 3:
                                        # This will set $this->mRevision if needed
 -                                      $this->fetchContent();
 +                                      $this->fetchContentObject();
  
                                        # Are we looking at an old revision
                                        if ( $oldid && $this->mRevision ) {
                                                wfDebug( __METHOD__ . ": showing CSS/JS source\n" );
                                                $this->showCssOrJsPage();
                                                $outputDone = true;
 -                                      } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
 +                                      } elseif( !wfRunHooks( 'ArticleContentViewCustom',
 +                                                                                      array( $this->fetchContentObject(), $this->getTitle(),
 +                                                                                                      $outputPage ) ) ) {
 +
 +                                              # Allow extensions do their own custom view for certain pages
 +                                              $outputDone = true;
 +                                      } elseif( !ContentHandler::runLegacyHooks( 'ArticleViewCustom',
 +                                                                                                                              array( $this->fetchContentObject(), $this->getTitle(),
 +                                                                                                                                              $outputPage ) ) ) {
 +
                                                # Allow extensions do their own custom view for certain pages
                                                $outputDone = true;
                                        } else {
 -                                              $text = $this->getContent();
 -                                              $rt = Title::newFromRedirectArray( $text );
 +                                              $content = $this->getContentObject();
 +                                              $rt = $content->getRedirectChain();
                                                if ( $rt ) {
                                                        wfDebug( __METHOD__ . ": showing redirect=no page\n" );
                                                        # Viewing a redirect page (e.g. with parameter redirect=no)
                                                        $outputPage->addHTML( $this->viewRedirect( $rt ) );
                                                        # Parse just to get categories, displaytitle, etc.
 -                                                      $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions );
 +                                                      $this->mParserOutput = $content->getParserOutput( $this->getTitle(), $oldid,
 +                                                                                                                                                              $parserOptions, false );
                                                        $outputPage->addParserOutputNoText( $this->mParserOutput );
                                                        $outputDone = true;
                                                }
                                        # Run the parse, protected by a pool counter
                                        wfDebug( __METHOD__ . ": doing uncached parse\n" );
  
 +                                      // @todo: shouldn't we be passing $this->getPage() to PoolWorkArticleView instead of plain $this?
                                        $poolArticleView = new PoolWorkArticleView( $this, $parserOptions,
 -                                              $this->getRevIdFetched(), $useParserCache, $this->getContent() );
 +                                              $this->getRevIdFetched(), $useParserCache, $this->getContentObject(), $this->getContext() );
  
                                        if ( !$poolArticleView->execute() ) {
                                                $error = $poolArticleView->getError();
        /**
         * Show a diff page according to current request variables. For use within
         * Article::view() only, other callers should use the DifferenceEngine class.
 +       *
 +       * @todo: make protected
         */
        public function showDiffPage() {
                $request = $this->getContext()->getRequest();
                $unhide = $request->getInt( 'unhide' ) == 1;
                $oldid = $this->getOldID();
  
 -              $de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
 +              $rev = $this->getRevisionFetched();
 +
 +              if ( !$rev ) {
 +                      $this->getContext()->getOutput()->setPageTitle( wfMessage( 'errorpagetitle' )->text() );
 +                      $this->getContext()->getOutput()->addWikiMsg( 'difference-missing-revision', $oldid, 1 );
 +                      return;
 +              }
 +
 +              $contentHandler = $rev->getContentHandler();
 +              $de = $contentHandler->createDifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
 +
                // DifferenceEngine directly fetched the revision:
                $this->mRevIdFetched = $de->mNewid;
                $de->showDiffPage( $diffOnly );
         * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these
         * page views.
         */
 -      protected function showCssOrJsPage() {
 -              $dir = $this->getContext()->getLanguage()->getDir();
 -              $lang = $this->getContext()->getLanguage()->getCode();
 +      protected function showCssOrJsPage( $showCacheHint = true ) {
 +              global $wgOut;
  
 -              $outputPage = $this->getContext()->getOutput();
 -              $outputPage->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
 -                      'clearyourcache' );
 +              if ( $showCacheHint ) {
 +                      $dir = $this->getContext()->getLanguage()->getDir();
 +                      $lang = $this->getContext()->getLanguage()->getCode();
 +
 +                      $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
 +                              'clearyourcache' );
 +              }
  
                // Give hooks a chance to customise the output
 -              if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
 -                      // Wrap the whole lot in a <pre> and don't parse
 -                      $m = array();
 -                      preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
 -                      $outputPage->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
 -                      $outputPage->addHTML( htmlspecialchars( $this->mContent ) );
 -                      $outputPage->addHTML( "\n</pre>\n" );
 +              if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->fetchContentObject(), $this->getTitle(), $wgOut ) ) ) {
 +                      $po = $this->mContentObject->getParserOutput( $this->getTitle() );
 +                      $wgOut->addHTML( $po->getText() );
                }
        }
  
  
                if ( $appendSubtitle ) {
                        $out = $this->getContext()->getOutput();
-                       $out->appendSubtitle( wfMessage( 'redirectpagesub' )->escaped() );
+                       $out->addSubtitle( wfMessage( 'redirectpagesub' )->escaped() );
                }
  
                // the loop prepends the arrow image before the link, so the first case needs to be outside
                // Generate deletion reason
                $hasHistory = false;
                if ( !$reason ) {
 -                      $reason = $this->generateReason( $hasHistory );
 +                      try {
 +                              $reason = $this->generateReason( $hasHistory );
 +                      } catch (MWException $e) {
 +                              # if a page is horribly broken, we still want to be able to delete it. so be lenient about errors here.
 +                              wfDebug("Error while building auto delete summary: $e");
 +                              $reason = '';
 +                      }
                }
  
                // If the page has a history, insert a warning
         * @return ParserOutput or false if the given revsion ID is not found
         */
        public function getParserOutput( $oldid = null, User $user = null ) {
 +              //XXX: bypasses mParserOptions and thus setParserOptions()
 +
                $user = is_null( $user ) ? $this->getContext()->getUser() : $user;
                $parserOptions = $this->mPage->makeParserOptions( $user );
  
                return $this->mPage->getParserOutput( $parserOptions, $oldid );
        }
  
 +      /**
 +       * Override the ParserOptions used to render the primary article wikitext.
 +       *
 +       * @param ParserOptions $options
 +       * @throws MWException if the parser options where already initialized.
 +       */
 +      public function setParserOptions( ParserOptions $options ) {
 +              if ( $this->mParserOptions ) {
 +                      throw new MWException( "can't change parser options after they have already been set" );
 +              }
 +
 +              // clone, so if $options is modified later, it doesn't confuse the parser cache.
 +              $this->mParserOptions = clone $options;
 +      }
 +
        /**
         * Get parser options suitable for rendering the primary article wikitext
         * @return ParserOptions
         * @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/Content.php
index 1b879c1,0000000..3bce48e
mode 100644,000000..100644
--- /dev/null
@@@ -1,1482 -1,0 +1,1482 @@@
-        * @param $title \Title the title of the deleted page
 +<?php
 +/**
 + * A content object represents page content, e.g. the text to show on a page.
 + * Content objects have no knowledge about how they relate to wiki pages.
 + *
 + * @since 1.WD
 + */
 +interface Content {
 +
 +      /**
 +       * @since WD.1
 +       *
 +       * @return string A string representing the content in a way useful for
 +       *   building a full text search index. If no useful representation exists,
 +       *   this method returns an empty string.
 +       *
 +       * @todo: test that this actually works
 +       * @todo: make sure this also works with LuceneSearch / WikiSearch
 +       */
 +      public function getTextForSearchIndex( );
 +
 +      /**
 +       * @since WD.1
 +       *
 +       * @return string The wikitext to include when another page includes this
 +       * content, or false if the content is not includable in a wikitext page.
 +       *
 +       * @TODO: allow native handling, bypassing wikitext representation, like
 +       *    for includable special pages.
 +       * @TODO: allow transclusion into other content models than Wikitext!
 +       * @TODO: used in WikiPage and MessageCache to get message text. Not so
 +       *    nice. What should we use instead?!
 +       */
 +      public function getWikitextForTransclusion( );
 +
 +      /**
 +       * Returns a textual representation of the content suitable for use in edit
 +       * summaries and log messages.
 +       *
 +       * @since WD.1
 +       *
 +       * @param $maxlength int Maximum length of the summary text
 +       * @return   The summary text
 +       */
 +      public function getTextForSummary( $maxlength = 250 );
 +
 +      /**
 +       * Returns native representation of the data. Interpretation depends on
 +       * the data model used, as given by getDataModel().
 +       *
 +       * @since WD.1
 +       *
 +       * @return mixed The native representation of the content. Could be a
 +       *    string, a nested array structure, an object, a binary blob...
 +       *    anything, really.
 +       *
 +       * @NOTE: review all calls carefully, caller must be aware of content model!
 +       */
 +      public function getNativeData( );
 +
 +      /**
 +       * Returns the content's nominal size in bogo-bytes.
 +       *
 +       * @return int
 +       */
 +      public function getSize( );
 +
 +      /**
 +       * Returns the ID of the content model used by this Content object.
 +       * Corresponds to the CONTENT_MODEL_XXX constants.
 +       *
 +       * @since WD.1
 +       *
 +       * @return String The model id
 +       */
 +      public function getModel();
 +
 +      /**
 +       * Convenience method that returns the ContentHandler singleton for handling
 +       * the content model that this Content object uses.
 +       *
 +       * Shorthand for ContentHandler::getForContent( $this )
 +       *
 +       * @since WD.1
 +       *
 +       * @return ContentHandler
 +       */
 +      public function getContentHandler();
 +
 +      /**
 +       * Convenience method that returns the default serialization format for the
 +       * content model that this Content object uses.
 +       *
 +       * Shorthand for $this->getContentHandler()->getDefaultFormat()
 +       *
 +       * @since WD.1
 +       *
 +       * @return String
 +       */
 +      public function getDefaultFormat();
 +
 +      /**
 +       * Convenience method that returns the list of serialization formats
 +       * supported for the content model that this Content object uses.
 +       *
 +       * Shorthand for $this->getContentHandler()->getSupportedFormats()
 +       *
 +       * @since WD.1
 +       *
 +       * @return Array of supported serialization formats
 +       */
 +      public function getSupportedFormats();
 +
 +      /**
 +       * Returns true if $format is a supported serialization format for this
 +       * Content object, false if it isn't.
 +       *
 +       * Note that this should always return true if $format is null, because null
 +       * stands for the default serialization.
 +       *
 +       * Shorthand for $this->getContentHandler()->isSupportedFormat( $format )
 +       *
 +       * @since WD.1
 +       *
 +       * @param $format string The format to check
 +       * @return bool Whether the format is supported
 +       */
 +      public function isSupportedFormat( $format );
 +
 +      /**
 +       * Convenience method for serializing this Content object.
 +       *
 +       * Shorthand for $this->getContentHandler()->serializeContent( $this, $format )
 +       *
 +       * @since WD.1
 +       *
 +       * @param $format null|string The desired serialization format (or null for
 +       *    the default format).
 +       * @return string Serialized form of this Content object
 +       */
 +      public function serialize( $format = null );
 +
 +      /**
 +       * Returns true if this Content object represents empty content.
 +       *
 +       * @since WD.1
 +       *
 +       * @return bool Whether this Content object is empty
 +       */
 +      public function isEmpty();
 +
 +      /**
 +       * Returns whether the content is valid. This is intended for local validity
 +       * checks, not considering global consistency.
 +       *
 +       * Content needs to be valid before it can be saved.
 +       *
 +       * This default implementation always returns true.
 +       *
 +       * @since WD.1
 +       *
 +       * @return boolean
 +       */
 +      public function isValid();
 +
 +      /**
 +       * Returns true if this Content objects is conceptually equivalent to the
 +       * given Content object.
 +       *
 +       * Contract:
 +       *
 +       * - Will return false if $that is null.
 +       * - Will return true if $that === $this.
 +       * - Will return false if $that->getModelName() != $this->getModel().
 +       * - Will return false if $that->getNativeData() is not equal to $this->getNativeData(),
 +       *   where the meaning of "equal" depends on the actual data model.
 +       *
 +       * Implementations should be careful to make equals() transitive and reflexive:
 +       *
 +       * - $a->equals( $b ) <=> $b->equals( $a )
 +       * - $a->equals( $b ) &&  $b->equals( $c ) ==> $a->equals( $c )
 +       *
 +       * @since WD.1
 +       *
 +       * @param $that Content The Content object to compare to
 +       * @return bool True if this Content object is equal to $that, false otherwise.
 +       */
 +      public function equals( Content $that = null );
 +
 +      /**
 +       * Return a copy of this Content object. The following must be true for the
 +       * object returned:
 +       *
 +       * if $copy = $original->copy()
 +       *
 +       * - get_class($original) === get_class($copy)
 +       * - $original->getModel() === $copy->getModel()
 +       * - $original->equals( $copy )
 +       *
 +       * If and only if the Content object is immutable, the copy() method can and
 +       * should return $this. That is,  $copy === $original may be true, but only
 +       * for immutable content objects.
 +       *
 +       * @since WD.1
 +       *
 +       * @return Content. A copy of this object
 +       */
 +      public function copy( );
 +
 +      /**
 +       * Returns true if this content is countable as a "real" wiki page, provided
 +       * that it's also in a countable location (e.g. a current revision in the
 +       * main namespace).
 +       *
 +       * @since WD.1
 +       *
 +       * @param $hasLinks Bool: If it is known whether this content contains
 +       *    links, provide this information here, to avoid redundant parsing to
 +       *    find out.
 +       * @return boolean
 +       */
 +      public function isCountable( $hasLinks = null ) ;
 +
 +
 +      /**
 +       * Parse the Content object and generate a ParserOutput from the result.
 +       * $result->getText() can be used to obtain the generated HTML. If no HTML
 +       * is needed, $generateHtml can be set to false; in that case,
 +       * $result->getText() may return null.
 +       *
 +       * @param $title Title The page title to use as a context for rendering
 +       * @param $revId null|int The revision being rendered (optional)
 +       * @param $options null|ParserOptions Any parser options
 +       * @param $generateHtml Boolean Whether to generate HTML (default: true). If false,
 +       *        the result of calling getText() on the ParserOutput object returned by
 +       *        this method is undefined.
 +       *
 +       * @since WD.1
 +       *
 +       * @return ParserOutput
 +       */
 +      public function getParserOutput( Title $title,
 +              $revId = null,
 +              ParserOptions $options = null, $generateHtml = true );
 +      # TODO: make RenderOutput and RenderOptions base classes
 +
 +      /**
 +       * Returns a list of DataUpdate objects for recording information about this
 +       * Content in some secondary data store. If the optional second argument,
 +       * $old, is given, the updates may model only the changes that need to be
 +       * made to replace information about the old content with information about
 +       * the new content.
 +       *
 +       * This default implementation calls
 +       * $this->getParserOutput( $content, $title, null, null, false ),
 +       * and then calls getSecondaryDataUpdates( $title, $recursive ) on the
 +       * resulting ParserOutput object.
 +       *
 +       * Subclasses may implement this to determine the necessary updates more
 +       * efficiently, or make use of information about the old content.
 +       *
 +       * @param $title Title The context for determining the necessary updates
 +       * @param $old Content|null An optional Content object representing the
 +       *    previous content, i.e. the content being replaced by this Content
 +       *    object.
 +       * @param $recursive boolean Whether to include recursive updates (default:
 +       *    false).
 +       * @param $parserOutput ParserOutput|null Optional ParserOutput object.
 +       *    Provide if you have one handy, to avoid re-parsing of the content.
 +       *
 +       * @return Array. A list of DataUpdate objects for putting information
 +       *    about this content object somewhere.
 +       *
 +       * @since WD.1
 +       */
 +      public function getSecondaryDataUpdates( Title $title,
 +              Content $old = null,
 +              $recursive = true, ParserOutput $parserOutput = null
 +      );
 +
 +      /**
 +       * Construct the redirect destination from this content and return an
 +       * array of Titles, or null if this content doesn't represent a redirect.
 +       * The last element in the array is the final destination after all redirects
 +       * have been resolved (up to $wgMaxRedirects times).
 +       *
 +       * @since WD.1
 +       *
 +       * @return Array of Titles, with the destination last
 +       */
 +      public function getRedirectChain();
 +
 +      /**
 +       * Construct the redirect destination from this content and return a Title,
 +       * or null if this content doesn't represent a redirect.
 +       * This will only return the immediate redirect target, useful for
 +       * the redirect table and other checks that don't need full recursion.
 +       *
 +       * @since WD.1
 +       *
 +       * @return Title: The corresponding Title
 +       */
 +      public function getRedirectTarget();
 +
 +      /**
 +       * Construct the redirect destination from this content and return the
 +       * Title, or null if this content doesn't represent a redirect.
 +       *
 +       * This will recurse down $wgMaxRedirects times or until a non-redirect
 +       * target is hit in order to provide (hopefully) the Title of the final
 +       * destination instead of another redirect.
 +       *
 +       * There is usually no need to override the default behaviour, subclasses that
 +       * want to implement redirects should override getRedirectTarget().
 +       * 
 +       * @since WD.1
 +       *
 +       * @return Title
 +       */
 +      public function getUltimateRedirectTarget();
 +
 +      /**
 +       * Returns whether this Content represents a redirect.
 +       * Shorthand for getRedirectTarget() !== null.
 +       *
 +       * @since WD.1
 +       *
 +       * @return bool
 +       */
 +      public function isRedirect();
 +
 +      /**
 +       * If this Content object is a redirect, this method updates the redirect target.
 +       * Otherwise, it does nothing.
 +       *
 +       * @since WD.1
 +       *
 +       * @param Title $target the new redirect target
 +       *
 +       * @return Content a new Content object with the updated redirect (or $this if this Content object isn't a redirect)
 +       */
 +      public function updateRedirect( Title $target );
 +
 +      /**
 +       * Returns the section with the given ID.
 +       *
 +       * @since WD.1
 +       *
 +       * @param $sectionId string The section's ID, given as a numeric string.
 +       *    The ID "0" retrieves the section before the first heading, "1" the
 +       *    text between the first heading (included) and the second heading
 +       *    (excluded), etc.
 +       * @return Content|Boolean|null The section, or false if no such section
 +       *    exist, or null if sections are not supported.
 +       */
 +      public function getSection( $sectionId );
 +
 +      /**
 +       * Replaces a section of the content and returns a Content object with the
 +       * section replaced.
 +       *
 +       * @since WD.1
 +       *
 +       * @param $section Empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
 +       * @param $with Content: new content of the section
 +       * @param $sectionTitle String: new section's subject, only if $section is 'new'
 +       * @return string Complete article text, or null if error
 +       */
 +      public function replaceSection( $section, Content $with, $sectionTitle = ''  );
 +
 +      /**
 +       * Returns a Content object with pre-save transformations applied (or this
 +       * object if no transformations apply).
 +       *
 +       * @since WD.1
 +       *
 +       * @param $title Title
 +       * @param $user User
 +       * @param $popts null|ParserOptions
 +       * @return Content
 +       */
 +      public function preSaveTransform( Title $title, User $user, ParserOptions $popts );
 +
 +      /**
 +       * Returns a new WikitextContent object with the given section heading
 +       * prepended, if supported. The default implementation just returns this
 +       * Content object unmodified, ignoring the section header.
 +       *
 +       * @since WD.1
 +       *
 +       * @param $header string
 +       * @return Content
 +       */
 +      public function addSectionHeader( $header );
 +
 +      /**
 +       * Returns a Content object with preload transformations applied (or this
 +       * object if no transformations apply).
 +       *
 +       * @since WD.1
 +       *
 +       * @param $title Title
 +       * @param $popts null|ParserOptions
 +       * @return Content
 +       */
 +      public function preloadTransform( Title $title, ParserOptions $popts );
 +
 +      /**
 +       * Prepare Content for saving. Called before Content is saved by WikiPage::doEditContent() and in
 +       * similar places.
 +       *
 +       * This may be used to check the content's consistency with global state. This function should
 +       * NOT write any information to the database.
 +       *
 +       * Note that this method will usually be called inside the same transaction bracket that will be used
 +       * to save the new revision.
 +       *
 +       * Note that this method is called before any update to the page table is performed. This means that
 +       * $page may not yet know a page ID.
 +       *
 +       * @param WikiPage $page The page to be saved.
 +       * @param int      $flags bitfield for use with EDIT_XXX constants, see WikiPage::doEditContent()
 +       * @param int      $baseRevId the ID of the current revision
 +       * @param User     $user
 +       *
 +       * @return Status A status object indicating whether the content was successfully prepared for saving.
 +       *                If the returned status indicates an error, a rollback will be performed and the
 +       *                transaction aborted.
 +       *
 +       * @see see WikiPage::doEditContent()
 +       */
 +      public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user );
 +
 +      /**
 +       * Returns a list of updates to perform when this content is deleted.
 +       * The necessary updates may be taken from the Content object, or depend on
 +       * the current state of the database.
 +       *
 +       * @since WD.1
 +       *
-       public function getDeletionUpdates( Title $title,
++       * @param $page \WikiPage the deleted page
 +       * @param $parserOutput null|\ParserOutput optional parser output object
 +       *    for efficient access to meta-information about the content object.
 +       *    Provide if you have one handy.
 +       *
 +       * @return array A list of DataUpdate instances that will clean up the
 +       *    database after deletion.
 +       */
-        * @param $title \Title the title of the deleted page
++      public function getDeletionUpdates( WikiPage $page,
 +              ParserOutput $parserOutput = null );
 +
 +      /**
 +       * Returns true if this Content object matches the given magic word.
 +       *
 +       * @param MagicWord $word the magic word to match
 +       *
 +       * @return bool whether this Content object matches the given magic word.
 +       */
 +      public function matchMagicWord( MagicWord $word );
 +
 +      # TODO: handle ImagePage and CategoryPage
 +      # TODO: make sure we cover lucene search / wikisearch.
 +      # TODO: make sure ReplaceTemplates still works
 +      # FUTURE: nice&sane integration of GeSHi syntax highlighting
 +      #   [11:59] <vvv> Hooks are ugly; make CodeHighlighter interface and a 
 +      #   config to set the class which handles syntax highlighting
 +      #   [12:00] <vvv> And default it to a DummyHighlighter
 +
 +      # TODO: make sure we cover the external editor interface (does anyone actually use that?!)
 +
 +      # TODO: tie into API to provide contentModel for Revisions
 +      # TODO: tie into API to provide serialized version and contentFormat for Revisions
 +      # TODO: tie into API edit interface
 +      # FUTURE: make EditForm plugin for EditPage
 +
 +      # FUTURE: special type for redirects?!
 +      # FUTURE: MultipartMultipart < WikipageContent (Main + Links + X)
 +      # FUTURE: LinksContent < LanguageLinksContent, CategoriesContent
 +}
 +
 +
 +/**
 + * A content object represents page content, e.g. the text to show on a page.
 + * Content objects have no knowledge about how they relate to Wiki pages.
 + *
 + * @since 1.WD
 + */
 +abstract class AbstractContent implements Content {
 +
 +      /**
 +       * Name of the content model this Content object represents.
 +       * Use with CONTENT_MODEL_XXX constants
 +       *
 +       * @var string $model_id
 +       */
 +      protected $model_id;
 +
 +      /**
 +       * @param String $model_id
 +       */
 +      public function __construct( $model_id = null ) {
 +              $this->model_id = $model_id;
 +      }
 +
 +      /**
 +       * @see Content::getModel()
 +       */
 +      public function getModel() {
 +              return $this->model_id;
 +      }
 +
 +      /**
 +       * Throws an MWException if $model_id is not the id of the content model
 +       * supported by this Content object.
 +       *
 +       * @param $model_id int the model to check
 +       *
 +       * @throws MWException
 +       */
 +      protected function checkModelID( $model_id ) {
 +              if ( $model_id !== $this->model_id ) {
 +                      throw new MWException( "Bad content model: " .
 +                              "expected {$this->model_id}  " .
 +                              "but got $model_id." );
 +              }
 +      }
 +
 +      /**
 +       * @see Content::getContentHandler()
 +       */
 +      public function getContentHandler() {
 +              return ContentHandler::getForContent( $this );
 +      }
 +
 +      /**
 +       * @see Content::getDefaultFormat()
 +       */
 +      public function getDefaultFormat() {
 +              return $this->getContentHandler()->getDefaultFormat();
 +      }
 +
 +      /**
 +       * @see Content::getSupportedFormats()
 +       */
 +      public function getSupportedFormats() {
 +              return $this->getContentHandler()->getSupportedFormats();
 +      }
 +
 +      /**
 +       * @see Content::isSupportedFormat()
 +       */
 +      public function isSupportedFormat( $format ) {
 +              if ( !$format ) {
 +                      return true; // this means "use the default"
 +              }
 +
 +              return $this->getContentHandler()->isSupportedFormat( $format );
 +      }
 +
 +      /**
 +       * Throws an MWException if $this->isSupportedFormat( $format ) doesn't
 +       * return true.
 +       *
 +       * @param $format
 +       * @throws MWException
 +       */
 +      protected function checkFormat( $format ) {
 +              if ( !$this->isSupportedFormat( $format ) ) {
 +                      throw new MWException( "Format $format is not supported for content model " . 
 +                              $this->getModel() );
 +              }
 +      }
 +
 +      /**
 +       * @see Content::serialize
 +       */
 +      public function serialize( $format = null ) {
 +              return $this->getContentHandler()->serializeContent( $this, $format );
 +      }
 +
 +      /**
 +       * @see Content::isEmpty()
 +       */
 +      public function isEmpty() {
 +              return $this->getSize() == 0;
 +      }
 +
 +      /**
 +       * @see Content::isValid()
 +       */
 +      public function isValid() {
 +              return true;
 +      }
 +
 +      /**
 +       * @see Content::equals()
 +       */
 +      public function equals( Content $that = null ) {
 +              if ( is_null( $that ) ) {
 +                      return false;
 +              }
 +
 +              if ( $that === $this ) {
 +                      return true;
 +              }
 +
 +              if ( $that->getModel() !== $this->getModel() ) {
 +                      return false;
 +              }
 +
 +              return $this->getNativeData() === $that->getNativeData();
 +      }
 +
 +
 +      /**
 +       * Returns a list of DataUpdate objects for recording information about this
 +       * Content in some secondary data store.
 +       *
 +       * This default implementation calls
 +       * $this->getParserOutput( $content, $title, null, null, false ),
 +       * and then calls getSecondaryDataUpdates( $title, $recursive ) on the
 +       * resulting ParserOutput object.
 +       *
 +       * Subclasses may override this to determine the secondary data updates more
 +       * efficiently, preferrably without the need to generate a parser output object.
 +       *
 +       * @see Content::getSecondaryDataUpdates()
 +       *
 +       * @param $title Title The context for determining the necessary updates
 +       * @param $old Content|null An optional Content object representing the
 +       *    previous content, i.e. the content being replaced by this Content
 +       *    object.
 +       * @param $recursive boolean Whether to include recursive updates (default:
 +       *    false).
 +       * @param $parserOutput ParserOutput|null Optional ParserOutput object.
 +       *    Provide if you have one handy, to avoid re-parsing of the content.
 +       *
 +       * @return Array. A list of DataUpdate objects for putting information
 +       *    about this content object somewhere.
 +       *
 +       * @since WD.1
 +       */
 +      public function getSecondaryDataUpdates( Title $title,
 +              Content $old = null,
 +              $recursive = true, ParserOutput $parserOutput = null
 +      ) {
 +              if ( !$parserOutput ) {
 +                      $parserOutput = $this->getParserOutput( $title, null, null, false );
 +              }
 +
 +              return $parserOutput->getSecondaryDataUpdates( $title, $recursive );
 +      }
 +
 +
 +      /**
 +       * @see Content::getRedirectChain()
 +       */
 +      public function getRedirectChain() {
 +              global $wgMaxRedirects;
 +              $title = $this->getRedirectTarget();
 +              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;
 +      }
 +
 +      /**
 +       * @see Content::getRedirectTarget()
 +       */
 +      public function getRedirectTarget() {
 +              return null;
 +      }
 +
 +      /**
 +       * @see Content::getUltimateRedirectTarget()
 +       * @note: migrated here from Title::newFromRedirectRecurse
 +       */
 +      public function getUltimateRedirectTarget() {
 +              $titles = $this->getRedirectChain();
 +              return $titles ? array_pop( $titles ) : null;
 +      }
 +
 +      /**
 +       * @see Content::isRedirect()
 +       *
 +       * @since WD.1
 +       *
 +       * @return bool
 +       */
 +      public function isRedirect() {
 +              return $this->getRedirectTarget() !== null;
 +      }
 +
 +      /**
 +       * @see Content::updateRedirect()
 +       *
 +       * This default implementation always returns $this.
 +       *
 +       * @since WD.1
 +       *
 +       * @return Content $this
 +       */
 +      public function updateRedirect( Title $target ) {
 +              return $this;
 +      }
 +
 +      /**
 +       * @see Content::getSection()
 +       */
 +      public function getSection( $sectionId ) {
 +              return null;
 +      }
 +
 +      /**
 +       * @see Content::replaceSection()
 +       */
 +      public function replaceSection( $section, Content $with, $sectionTitle = ''  ) {
 +              return null;
 +      }
 +
 +      /**
 +       * @see Content::preSaveTransform()
 +       */
 +      public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
 +              return $this;
 +      }
 +
 +      /**
 +       * @see Content::addSectionHeader()
 +       */
 +      public function addSectionHeader( $header ) {
 +              return $this;
 +      }
 +
 +      /**
 +       * @see Content::preloadTransform()
 +       */
 +      public function preloadTransform( Title $title, ParserOptions $popts ) {
 +              return $this;
 +      }
 +
 +      /**
 +       * @see  Content::prepareSave()
 +       */
 +      public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user ) {
 +              if ( $this->isValid() ) {
 +                      return Status::newGood();
 +              } else {
 +                      return Status::newFatal( "invalid-content-data" );
 +              }
 +      }
 +
 +      /**
 +       * @see  Content::getDeletionUpdates()
 +       *
 +       * @since WD.1
 +       *
-       public function getDeletionUpdates( Title $title,
++       * @param $page \WikiPage the deleted page
 +       * @param $parserOutput null|\ParserOutput optional parser output object
 +       *    for efficient access to meta-information about the content object.
 +       *    Provide if you have one handy.
 +       *
 +       * @return array A list of DataUpdate instances that will clean up the
 +       *    database after deletion.
 +       */
-                       new LinksDeletionUpdate( $title ),
++      public function getDeletionUpdates( WikiPage $page,
 +              ParserOutput $parserOutput = null )
 +      {
 +              return array(
++                      new LinksDeletionUpdate( $page ),
 +              );
 +      }
 +
 +      /**
 +       * @see  Content::matchMagicWord()
 +       *
 +       * This default implementation always returns false. Subclasses may override this to supply matching logic.
 +       *
 +       * @param MagicWord $word
 +       *
 +       * @return bool
 +       */
 +      public function matchMagicWord( MagicWord $word ) {
 +              return false;
 +      }
 +}
 +
 +/**
 + * Content object implementation for representing flat text.
 + *
 + * TextContent instances are immutable
 + *
 + * @since WD.1
 + */
 +abstract class TextContent extends AbstractContent {
 +
 +      public function __construct( $text, $model_id = null ) {
 +              parent::__construct( $model_id );
 +
 +              $this->mText = $text;
 +      }
 +
 +      public function copy() {
 +              return $this; # NOTE: this is ok since TextContent are immutable.
 +      }
 +
 +      public function getTextForSummary( $maxlength = 250 ) {
 +              global $wgContLang;
 +
 +              $text = $this->getNativeData();
 +
 +              $truncatedtext = $wgContLang->truncate(
 +                      preg_replace( "/[\n\r]/", ' ', $text ),
 +                      max( 0, $maxlength ) );
 +
 +              return $truncatedtext;
 +      }
 +
 +      /**
 +       * returns the text's size in bytes.
 +       *
 +       * @return int The size
 +       */
 +      public function getSize( ) {
 +              $text = $this->getNativeData( );
 +              return strlen( $text );
 +      }
 +
 +      /**
 +       * Returns true if this content is not a redirect, and $wgArticleCountMethod
 +       * is "any".
 +       *
 +       * @param $hasLinks Bool: if it is known whether this content contains links,
 +       * provide this information here, to avoid redundant parsing to find out.
 +       *
 +       * @return bool True if the content is countable
 +       */
 +      public function isCountable( $hasLinks = null ) {
 +              global $wgArticleCountMethod;
 +
 +              if ( $this->isRedirect( ) ) {
 +                      return false;
 +              }
 +
 +              if (  $wgArticleCountMethod === 'any' ) {
 +                      return true;
 +              }
 +
 +              return false;
 +      }
 +
 +      /**
 +       * Returns the text represented by this Content object, as a string.
 +       *
 +       * @param   the raw text
 +       */
 +      public function getNativeData( ) {
 +              $text = $this->mText;
 +              return $text;
 +      }
 +
 +      /**
 +       * Returns the text represented by this Content object, as a string.
 +       *
 +       * @param   the raw text
 +       */
 +      public function getTextForSearchIndex( ) {
 +              return $this->getNativeData();
 +      }
 +
 +      /**
 +       * Returns the text represented by this Content object, as a string.
 +       *
 +       * @param   the raw text
 +       */
 +      public function getWikitextForTransclusion( ) {
 +              return $this->getNativeData();
 +      }
 +
 +      /**
 +       * Diff this content object with another content object..
 +       *
 +       * @since WD.diff
 +       *
 +       * @param $that Content the other content object to compare this content object to
 +       * @param $lang Language the language object to use for text segmentation.
 +       *    If not given, $wgContentLang is used.
 +       *
 +       * @return DiffResult a diff representing the changes that would have to be
 +       *    made to this content object to make it equal to $that.
 +       */
 +      public function diff( Content $that, Language $lang = null ) {
 +              global $wgContLang;
 +
 +              $this->checkModelID( $that->getModel() );
 +
 +              # @todo: could implement this in DifferenceEngine and just delegate here?
 +
 +              if ( !$lang ) $lang = $wgContLang;
 +
 +              $otext = $this->getNativeData();
 +              $ntext = $this->getNativeData();
 +
 +              # Note: Use native PHP diff, external engines don't give us abstract output
 +              $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
 +              $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
 +
 +              $diff = new Diff( $ota, $nta );
 +              return $diff;
 +      }
 +
 +
 +      /**
 +       * Returns a generic ParserOutput object, wrapping the HTML returned by
 +       * getHtml().
 +       *
 +       * @param $title Title Context title for parsing
 +       * @param $revId int|null Revision ID (for {{REVISIONID}})
 +       * @param $options ParserOptions|null Parser options
 +       * @param $generateHtml bool Whether or not to generate HTML
 +       *
 +       * @return ParserOutput representing the HTML form of the text
 +       */
 +      public function getParserOutput( Title $title,
 +              $revId = null,
 +              ParserOptions $options = null, $generateHtml = true
 +      ) {
 +              # Generic implementation, relying on $this->getHtml()
 +
 +              if ( $generateHtml ) {
 +                      $html = $this->getHtml();
 +              } else {
 +                      $html = '';
 +              }
 +
 +              $po = new ParserOutput( $html );
 +              return $po;
 +      }
 +
 +      /**
 +       * Generates an HTML version of the content, for display. Used by
 +       * getParserOutput() to construct a ParserOutput object.
 +       *
 +       * This default implementation just calls getHighlightHtml(). Content
 +       * models that have another mapping to HTML (as is the case for markup
 +       * languages like wikitext) should override this method to generate the
 +       * appropriate HTML.
 +       *
 +       * @return string An HTML representation of the content
 +       */
 +      protected function getHtml() {
 +              return $this->getHighlightHtml();
 +      }
 +
 +      /**
 +       * Generates a syntax-highlighted version of the content, as HTML.
 +       * Used by the default implementation of getHtml().
 +       *
 +       * @return string an HTML representation of the content's markup
 +       */
 +      protected function getHighlightHtml( ) {
 +              # TODO: make Highlighter interface, use highlighter here, if available
 +              return htmlspecialchars( $this->getNativeData() );
 +      }
 +}
 +
 +/**
 + * @since WD.1
 + */
 +class WikitextContent extends TextContent {
 +
 +      public function __construct( $text ) {
 +              parent::__construct( $text, CONTENT_MODEL_WIKITEXT );
 +      }
 +
 +      /**
 +       * @see Content::getSection()
 +       */
 +      public function getSection( $section ) {
 +              global $wgParser;
 +
 +              $text = $this->getNativeData();
 +              $sect = $wgParser->getSection( $text, $section, false );
 +
 +              return  new WikitextContent( $sect );
 +      }
 +
 +      /**
 +       * @see Content::replaceSection()
 +       */
 +      public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
 +              wfProfileIn( __METHOD__ );
 +
 +              $myModelId = $this->getModel();
 +              $sectionModelId = $with->getModel();
 +
 +              if ( $sectionModelId != $myModelId  ) {
 +                      throw new MWException( "Incompatible content model for section: " .
 +                              "document uses $myModelId but " .
 +                              "section uses $sectionModelId." );
 +              }
 +
 +              $oldtext = $this->getNativeData();
 +              $text = $with->getNativeData();
 +
 +              if ( $section === '' ) {
 +                      return $with; # XXX: copy first?
 +              } if ( $section == 'new' ) {
 +                      # Inserting a new section
 +                      $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' )
 +                              ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : '';
 +                      if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
 +                              $text = strlen( trim( $oldtext ) ) > 0
 +                                      ? "{$oldtext}\n\n{$subject}{$text}"
 +                                      : "{$subject}{$text}";
 +                      }
 +              } else {
 +                      # Replacing an existing section; roll out the big guns
 +                      global $wgParser;
 +
 +                      $text = $wgParser->replaceSection( $oldtext, $section, $text );
 +              }
 +
 +              $newContent = new WikitextContent( $text );
 +
 +              wfProfileOut( __METHOD__ );
 +              return $newContent;
 +      }
 +
 +      /**
 +       * Returns a new WikitextContent object with the given section heading
 +       * prepended.
 +       *
 +       * @param $header string
 +       * @return Content
 +       */
 +      public function addSectionHeader( $header ) {
 +              $text = wfMessage( 'newsectionheaderdefaultlevel' )
 +                              ->inContentLanguage()->params( $header )->text();
 +              $text .= "\n\n";
 +              $text .= $this->getNativeData();
 +
 +              return new WikitextContent( $text );
 +      }
 +
 +      /**
 +       * Returns a Content object with pre-save transformations applied using
 +       * Parser::preSaveTransform().
 +       *
 +       * @param $title Title
 +       * @param $user User
 +       * @param $popts ParserOptions
 +       * @return Content
 +       */
 +      public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
 +              global $wgParser;
 +
 +              $text = $this->getNativeData();
 +              $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
 +
 +              return new WikitextContent( $pst );
 +      }
 +
 +      /**
 +       * Returns a Content object with preload transformations applied (or this
 +       * object if no transformations apply).
 +       *
 +       * @param $title Title
 +       * @param $popts ParserOptions
 +       * @return Content
 +       */
 +      public function preloadTransform( Title $title, ParserOptions $popts ) {
 +              global $wgParser;
 +
 +              $text = $this->getNativeData();
 +              $plt = $wgParser->getPreloadText( $text, $title, $popts );
 +
 +              return new WikitextContent( $plt );
 +      }
 +
 +      /**
 +       * Implement redirect extraction for wikitext.
 +       *
 +       * @return null|Title
 +       *
 +       * @note: migrated here from Title::newFromRedirectInternal()
 +       *
 +       * @see Content::getRedirectTarget
 +       * @see AbstractContent::getRedirectTarget
 +       */
 +      public function getRedirectTarget() {
 +              global $wgMaxRedirects;
 +              if ( $wgMaxRedirects < 1 ) {
 +                      // redirects are disabled, so quit early
 +                      return null;
 +              }
 +              $redir = MagicWord::get( 'redirect' );
 +              $text = trim( $this->getNativeData() );
 +              if ( $redir->matchStartAndRemove( $text ) ) {
 +                      // Extract the first link and see if it's usable
 +                      // Ensure that it really does come directly after #REDIRECT
 +                      // Some older redirects included a colon, so don't freak about that!
 +                      $m = array();
 +                      if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
 +                              // Strip preceding colon used to "escape" categories, etc.
 +                              // and URL-decode links
 +                              if ( strpos( $m[1], '%' ) !== false ) {
 +                                      // Match behavior of inline link parsing here;
 +                                      $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
 +                              }
 +                              $title = Title::newFromText( $m[1] );
 +                              // If the title is a redirect to bad special pages or is invalid, return null
 +                              if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
 +                                      return null;
 +                              }
 +                              return $title;
 +                      }
 +              }
 +              return null;
 +      }
 +
 +      /**
 +       * @see   Content::updateRedirect()
 +       *
 +       * This implementation replaces the first link on the page with the given new target
 +       * if this Content object is a redirect. Otherwise, this method returns $this.
 +       *
 +       * @since WD.1
 +       *
 +       * @param Title $target
 +       *
 +       * @return Content a new Content object with the updated redirect (or $this if this Content object isn't a redirect)
 +       */
 +      public function updateRedirect( Title $target ) {
 +              if ( !$this->isRedirect() ) {
 +                      return $this;
 +              }
 +
 +              # Fix the text
 +              # Remember that redirect pages can have categories, templates, etc.,
 +              # so the regex has to be fairly general
 +              $newText = preg_replace( '/ \[ \[  [^\]]*  \] \] /x',
 +                      '[[' . $target->getFullText() . ']]',
 +                      $this->getNativeData(), 1 );
 +
 +              return new WikitextContent( $newText );
 +      }
 +
 +      /**
 +       * Returns true if this content is not a redirect, and this content's text
 +       * is countable according to the criteria defined by $wgArticleCountMethod.
 +       *
 +       * @param $hasLinks Bool  if it is known whether this content contains
 +       *    links, provide this information here, to avoid redundant parsing to
 +       *    find out.
 +       * @param $title null|\Title
 +       *
 +       * @internal param \IContextSource $context context for parsing if necessary
 +       *
 +       * @return bool True if the content is countable
 +       */
 +      public function isCountable( $hasLinks = null, Title $title = null ) {
 +              global $wgArticleCountMethod;
 +
 +              if ( $this->isRedirect( ) ) {
 +                      return false;
 +              }
 +
 +              $text = $this->getNativeData();
 +
 +              switch ( $wgArticleCountMethod ) {
 +                      case 'any':
 +                              return true;
 +                      case 'comma':
 +                              return strpos( $text,  ',' ) !== false;
 +                      case 'link':
 +                              if ( $hasLinks === null ) { # not known, find out
 +                                      if ( !$title ) {
 +                                              $context = RequestContext::getMain();
 +                                              $title = $context->getTitle();
 +                                      }
 +
 +                                      $po = $this->getParserOutput( $title, null, null, false );
 +                                      $links = $po->getLinks();
 +                                      $hasLinks = !empty( $links );
 +                              }
 +
 +                              return $hasLinks;
 +              }
 +
 +              return false;
 +      }
 +
 +      public function getTextForSummary( $maxlength = 250 ) {
 +              $truncatedtext = parent::getTextForSummary( $maxlength );
 +
 +              # clean up unfinished links
 +              # XXX: make this optional? wasn't there in autosummary, but required for
 +              # deletion summary.
 +              $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );
 +
 +              return $truncatedtext;
 +      }
 +
 +
 +      /**
 +       * Returns a ParserOutput object resulting from parsing the content's text
 +       * using $wgParser.
 +       *
 +       * @since    WD.1
 +       *
 +       * @param $content Content the content to render
 +       * @param $title \Title
 +       * @param $revId null
 +       * @param $options null|ParserOptions
 +       * @param $generateHtml bool
 +       *
 +       * @internal param \IContextSource|null $context
 +       * @return ParserOutput representing the HTML form of the text
 +       */
 +      public function getParserOutput( Title $title,
 +              $revId = null,
 +              ParserOptions $options = null, $generateHtml = true
 +      ) {
 +              global $wgParser;
 +
 +              if ( !$options ) {
 +                      $options = new ParserOptions();
 +              }
 +
 +              $po = $wgParser->parse( $this->getNativeData(), $title, $options, true, true, $revId );
 +              return $po;
 +      }
 +
 +      protected function getHtml() {
 +              throw new MWException(
 +                      "getHtml() not implemented for wikitext. "
 +                              . "Use getParserOutput()->getText()."
 +              );
 +      }
 +
 +      /**
 +       * @see  Content::matchMagicWord()
 +       *
 +       * This implementation calls $word->match() on the this TextContent object's text.
 +       *
 +       * @param MagicWord $word
 +       *
 +       * @return bool whether this Content object matches the given magic word.
 +       */
 +      public function matchMagicWord( MagicWord $word ) {
 +              return $word->match( $this->getNativeData() );
 +      }
 +}
 +
 +/**
 + * Wrapper allowing us to handle a system message as a Content object. Note that this is generally *not* used
 + * to represent content from the MediaWiki namespace, and that there is no MessageContentHandler. MessageContent
 + * is just intended as glue for wrapping a message programatically.
 + *
 + * @since WD.1
 + */
 +class MessageContent extends AbstractContent {
 +
 +      /**
 +       * @var Message
 +       */
 +      protected $mMessage;
 +
 +      /**
 +       * @param Message|String $msg    A Message object, or a message key
 +       * @param array|null     $params An optional array of message parameters
 +       */
 +      public function __construct( $msg, $params = null ) {
 +              # XXX: messages may be wikitext, html or plain text! and maybe even something else entirely.
 +              parent::__construct( CONTENT_MODEL_WIKITEXT );
 +
 +              if ( is_string( $msg ) ) {
 +                      $this->mMessage = wfMessage( $msg );
 +              } else {
 +                      $this->mMessage = clone $msg;
 +              }
 +
 +              if ( $params ) {
 +                      $this->mMessage = $this->mMessage->params( $params );
 +              }
 +      }
 +
 +      /**
 +       * Returns the message as rendered HTML
 +       *
 +       * @return string The message text, parsed into html
 +       */
 +      public function getHtml() {
 +              return $this->mMessage->parse();
 +      }
 +
 +      /**
 +       * Returns the message as rendered HTML
 +       *
 +       * @return string The message text, parsed into html
 +       */
 +      public function getWikitext() {
 +              return $this->mMessage->text();
 +      }
 +
 +      /**
 +       * Returns the message object, with any parameters already substituted.
 +       *
 +       * @return Message The message object.
 +       */
 +      public function getNativeData() {
 +              //NOTE: Message objects are mutable. Cloning here makes MessageContent immutable.
 +              return clone $this->mMessage;
 +      }
 +
 +      /**
 +       * @see Content::getTextForSearchIndex
 +       */
 +      public function getTextForSearchIndex() {
 +              return $this->mMessage->plain();
 +      }
 +
 +      /**
 +       * @see Content::getWikitextForTransclusion
 +       */
 +      public function getWikitextForTransclusion() {
 +              return $this->getWikitext();
 +      }
 +
 +      /**
 +       * @see Content::getTextForSummary
 +       */
 +      public function getTextForSummary( $maxlength = 250 ) {
 +              return substr( $this->mMessage->plain(), 0, $maxlength );
 +      }
 +
 +      /**
 +       * @see Content::getSize
 +       *
 +       * @return int
 +       */
 +      public function getSize() {
 +              return strlen( $this->mMessage->plain() );
 +      }
 +
 +      /**
 +       * @see Content::copy
 +       *
 +       * @return Content. A copy of this object
 +       */
 +      public function copy() {
 +              // MessageContent is immutable (because getNativeData() returns a clone of the Message object)
 +              return $this;
 +      }
 +
 +      /**
 +       * @see Content::isCountable
 +       *
 +       * @return bool false
 +       */
 +      public function isCountable( $hasLinks = null ) {
 +              return false;
 +      }
 +
 +      /**
 +       * @see Content::getParserOutput
 +       *
 +       * @return ParserOutput
 +       */
 +      public function getParserOutput(
 +              Title $title, $revId = null,
 +              ParserOptions $options = null, $generateHtml = true
 +      ) {
 +
 +              if ( $generateHtml ) {
 +                      $html = $this->getHtml();
 +              } else {
 +                      $html = '';
 +              }
 +
 +              $po = new ParserOutput( $html );
 +              return $po;
 +      }
 +}
 +
 +/**
 + * @since WD.1
 + */
 +class JavaScriptContent extends TextContent {
 +      public function __construct( $text ) {
 +              parent::__construct( $text, CONTENT_MODEL_JAVASCRIPT );
 +      }
 +
 +      /**
 +       * Returns a Content object with pre-save transformations applied using
 +       * Parser::preSaveTransform().
 +       *
 +       * @param Title $title
 +       * @param User $user
 +       * @param ParserOptions $popts
 +       * @return Content
 +       */
 +      public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
 +              global $wgParser;
 +              // @todo: make pre-save transformation optional for script pages
 +              // See bug #32858
 +
 +              $text = $this->getNativeData();
 +              $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
 +
 +              return new JavaScriptContent( $pst );
 +      }
 +
 +
 +      protected function getHtml( ) {
 +              $html = "";
 +              $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
 +              $html .= $this->getHighlightHtml( );
 +              $html .= "\n</pre>\n";
 +
 +              return $html;
 +      }
 +}
 +
 +/**
 + * @since WD.1
 + */
 +class CssContent extends TextContent {
 +      public function __construct( $text ) {
 +              parent::__construct( $text, CONTENT_MODEL_CSS );
 +      }
 +
 +      /**
 +       * Returns a Content object with pre-save transformations applied using
 +       * Parser::preSaveTransform().
 +       *
 +       * @param $title Title
 +       * @param $user User
 +       * @param $popts ParserOptions
 +       * @return Content
 +       */
 +      public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
 +              global $wgParser;
 +              // @todo: make pre-save transformation optional for script pages
 +
 +              $text = $this->getNativeData();
 +              $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
 +
 +              return new CssContent( $pst );
 +      }
 +
 +
 +      protected function getHtml( ) {
 +              $html = "";
 +              $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
 +              $html .= $this->getHighlightHtml( );
 +              $html .= "\n</pre>\n";
 +
 +              return $html;
 +      }
 +}
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() ) ) );
                        }
                }
  
 +              $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
 +       * @todo: deprecated, replace usage everywhere
         */
 -      function getContent( $def_text = '' ) {
 -              global $wgOut, $wgRequest, $wgParser;
 +      function getContent( $def_text = false ) {
 +              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 );
 +
 +              // Note: EditPage should only be used with text based content anyway.
 +              return $content->serialize( $this->content_format );
 +      }
 +
 +      private function getContentObject( $def_content = null ) {
 +              global $wgOut, $wgRequest;
  
                wfProfileIn( __METHOD__ );
  
 -              $text = false;
 +              $content = false;
  
                // For message page not locally set, use the i18n message.
                // For other non-existent articles, use preload text if any.
                if ( !$this->mTitle->exists() || $this->section == 'new' ) {
                        if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
                                # If this is a system message, get the default text.
 -                              $text = $this->mTitle->getDefaultMessageText();
 +                              $msg = $this->mTitle->getDefaultMessageText();
 +
 +                              $content = 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 {
                                                wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
                                }
  
 -                              if ( $text === false ) {
 -                                      $text = $this->getOriginalContent();
 +                              if ( $content === false ) {
 +                                      $content = $this->getOriginalContent();
                                }
                        }
                }
  
                wfProfileOut( __METHOD__ );
 -              return $text;
 +              return $content;
        }
  
        /**
         */
        private function getOriginalContent() {
                if ( $this->section == 'new' ) {
 -                      return $this->getCurrentText();
 +                      return $this->getCurrentContent();
                }
                $revision = $this->mArticle->getRevisionFetched();
                if ( $revision === null ) {
 -                      return '';
 +                      if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModel();
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +
 +                      return $handler->makeEmptyContent();
                }
 -              return $this->mArticle->getContent();
 +              $content = $revision->getContent();
 +              return $content;
        }
  
        /**
 -       * Get the actual text of the page. This is basically similar to
 -       * WikiPage::getRawText() except that when the page doesn't exist an empty
 -       * string is returned instead of false.
 +       * Get the current content of the page. This is basically similar to
 +       * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
 +       * content object is returned instead of null.
         *
 -       * @since 1.19
 +       * @since 1.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;
 +                      # nasty side-effect, but needed for consistency
 +                      $this->content_model = $rev->getContentModel();
 +                      $this->content_format = $rev->getContentFormat();
 +
 +                      return $content;
                }
        }
  
 +
        /**
         * Use this method before edit() to preload some text into the edit box
         *
         * @param $text string
 +       * @deprecated since 1.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 ) { #NOTE: B/C only, replace usage!
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              $content = $this->getPreloadedContent( $preload );
 +              $text = $content->serialize( $this->content_format );
 +
 +              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 getPreloadedText( $preload ) {
 -              global $wgUser, $wgParser;
 +      protected function getPreloadedContent( $preload ) { #@todo: use this!
 +              global $wgUser;
  
 -              if ( !empty( $this->mPreloadText ) ) {
 -                      return $this->mPreloadText;
 +              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>');
 +                              return true;
 +
                        case self::AS_SUCCESS_NEW_ARTICLE:
                                $query = $resultDetails['redirect'] ? 'redirect=no' : '';
                                $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
                        return $status;
                }
  
 +              try {
 +                      # Construct Content object
 +                      $textbox_content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(),
 +                                                                                                                      $this->content_model, $this->content_format );
 +              } catch (MWContentSerializationException $ex) {
 +                      $status->fatal( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $status->value = self::AS_PARSE_ERROR;
 +                      wfProfileOut( __METHOD__ );
 +                      return $status;
 +              }
 +
                # Check image redirect
                if ( $this->mTitle->getNamespace() == NS_FILE &&
 -                      Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
 +                      $textbox_content->isRedirect() &&
                        !$wgUser->isAllowed( 'upload' ) ) {
                                $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
                                $status->setResult( false, $code );
                                return $status;
                        }
  
 -                      $text = $this->textbox1;
 +                      $content = $textbox_content;
 +
                        $result['sectionanchor'] = '';
                        if ( $this->section == 'new' ) {
                                if ( $this->sectiontitle !== '' ) {
                                        // Insert the section title above the content.
 -                                      $text = wfMessage( 'newsectionheaderdefaultlevel', $this->sectiontitle )
 -                                              ->inContentLanguage()->text() . "\n\n" . $text;
 +                                      $content = $content->addSectionHeader( $this->sectiontitle );
  
                                        // Jump to the new section
                                        $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
                                        if ( $this->summary === '' ) {
                                                $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
                                                $this->summary = wfMessage( 'newsectionsummary', $cleanSectionTitle )
 -                                                      ->inContentLanguage()->text();
 +                                                      ->inContentLanguage()->text() ;
                                        }
                                } elseif ( $this->summary !== '' ) {
                                        // Insert the section title above the content.
 -                                      $text = wfMessage( 'newsectionheaderdefaultlevel', $this->summary )
 -                                              ->inContentLanguage()->text() . "\n\n" . $text;
 +                                      $content = $content->addSectionHeader( $this->summary );
  
                                        // Jump to the new section
                                        $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
  
                        $status->value = self::AS_SUCCESS_NEW_ARTICLE;
  
 -              } else {
 +              } else { # not $new
  
                        # Article exists. Check for edit conflict.
 +
 +                      $this->mArticle->clear(); # Force reload of dates, etc.
                        $timestamp = $this->mArticle->getTimestamp();
 +
                        wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
  
                        if ( $timestamp != $this->edittime ) {
                                $sectionTitle = $this->summary;
                        }
  
 +                      $content = null;
 +
                        if ( $this->isConflict ) {
 -                              wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
 -                              $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle, $this->edittime );
 +                              wfDebug( __METHOD__ . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
 +                                              . " (article time '{$timestamp}')\n" );
 +
 +                              $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content,
 +                                                                                                                                      $sectionTitle, $this->edittime );
                        } else {
 -                              wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
 -                              $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle );
 +                              wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
 +                              $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle );
                        }
 -                      if ( is_null( $text ) ) {
 +
 +                      if ( is_null( $content ) ) {
                                wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
                                $this->isConflict = true;
 -                              $text = $this->textbox1; // do not try to merge here!
 +                              $content = $textbox_content; // do not try to merge here!
                        } elseif ( $this->isConflict ) {
                                # Attempt merge
 -                              if ( $this->mergeChangesInto( $text ) ) {
 +                              if ( $this->mergeChangesIntoContent( $textbox_content ) ) {
                                        // Successful merge! Maybe we should tell the user the good news?
                                        $this->isConflict = false;
 +                                      $content = $textbox_content;
                                        wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
                                } else {
                                        $this->section = '';
 -                                      $this->textbox1 = $text;
 +                                      #$this->textbox1 = $text; #redundant, nothing to do here?
                                        wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
                                }
                        }
                        }
  
                        // Run post-section-merge edit filter
 -                      if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) {
 +                      $hook_args = array( $this, $content, &$this->hookError, $this->summary );
 +
 +                      if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged', $hook_args )
 +                              || !wfRunHooks( 'EditFilterMergedContent', $hook_args ) ) {
                                # Error messages etc. could be handled within the hook...
                                $status->fatal( 'hookaborted' );
                                $status->value = self::AS_HOOK_ERROR;
  
                        # Handle the user preference to force summaries here, but not for null edits
                        if ( $this->section != 'new' && !$this->allowBlankSummary
 -                              && $this->getOriginalContent() != $text
 -                              && !Title::newFromRedirect( $text ) ) # check if it's not a redirect
 +                              && !$content->equals( $this->getOriginalContent() )
 +                              && !$content->isRedirect() ) # check if it's not a redirect
                        {
                                if ( md5( $this->summary ) == $this->autoSumm ) {
                                        $this->missingSummary = true;
                        // merged the section into full text. Clear the section field
                        // so that later submission of conflict forms won't try to
                        // replace that into a duplicated mess.
 -                      $this->textbox1 = $text;
 +                      $this->textbox1 = $content->serialize( $this->content_format );
                        $this->section = '';
  
                        $status->value = self::AS_SUCCESS_UPDATE;
                }
  
                // Check for length errors again now that the section is merged in
 -              $this->kblength = (int)( strlen( $text ) / 1024 );
 +                      $this->kblength = (int)( strlen( $content->serialize( $this->content_format ) ) / 1024 );
                if ( $this->kblength > $wgMaxArticleSize ) {
                        $this->tooBig = true;
                        $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
                        ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
                        ( $bot ? EDIT_FORCE_BOT : 0 );
  
 -              $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags );
 +                      $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags,
 +                                                                                                                      false, null, $this->content_format );
  
                if ( $doEditStatus->isOK() ) {
 -                      $result['redirect'] = Title::newFromRedirect( $text ) !== null;
 +                              $result['redirect'] = $content->isRedirect();
                        $this->commitWatch();
                        wfProfileOut( __METHOD__ );
                        return $status;
         * @param $editText string
         *
         * @return bool
 +       * @deprecated since 1.WD, use mergeChangesIntoContent() instead
 +       */
 +      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 );
 +                      return true;
 +              } else {
 +                      return false;
 +              }
 +      }
 +
 +      /**
 +       * @private
 +       * @todo document
 +       *
 +       * @parma $editText string
 +       *
 +       * @return bool
 +       * @since since 1.WD
         */
 -      function mergeChangesInto( &$editText ) {
 +      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();
  
 -              $result = '';
 -              if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
 -                      $editText = $result;
 +              $handler = ContentHandler::getForModelID( $baseContent->getModel() );
 +
 +              $result = $handler->merge3( $baseContent, $editContent, $currentContent );
 +
 +              if ( $result ) {
 +                      $editContent = $result;
                        wfProfileOut( __METHOD__ );
                        return true;
                } else {
                        }
                }
  
 +              //@todo: add EditForm plugin interface and use it here!
 +              //       search for textarea1 and textares2, and allow EditForm to override all uses.
                $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID,
                        'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ),
                        'enctype' => 'multipart/form-data' ) ) );
  
                $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
  
 +              $wgOut->addHTML( Html::hidden( 'format', $this->content_format ) );
 +              $wgOut->addHTML( Html::hidden( 'model', $this->content_model ) );
 +
                if ( $this->section == 'new' ) {
                        $this->showSummaryInput( true, $this->summary );
                        $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
                        // resolved between page source edits and custom ui edits using the
                        // custom edit ui.
                        $this->textbox2 = $this->textbox1;
 -                      $this->textbox1 = $this->getCurrentText();
 +
 +                      $content = $this->getCurrentContent();
 +                      $this->textbox1 = $content->serialize( $this->content_format );
  
                        $this->showTextbox1();
                } else {
                        Linker::formatHiddenCategories( $this->mArticle->getHiddenCategories() ) ) );
  
                if ( $this->isConflict ) {
 -                      $this->showConflict();
 +                      try {
 +                              $this->showConflict();
 +                      } catch ( MWContentSerializationException $ex ) {
 +                              // this can't really happen, but be nice if it does.
 +                              $msg = wfMessage( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                              $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>');
 +                      }
                }
  
                $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
  
                        if ( $this->section != '' && $this->section != 'new' ) {
                                if ( !$this->summary && !$this->preview && !$this->diff ) {
 -                                      $sectionTitle = self::extractSectionTitle( $this->textbox1 );
 +                                      $sectionTitle = self::extractSectionTitle( $this->textbox1 ); //FIXME: use Content object
                                        if ( $sectionTitle !== false ) {
                                                $this->summary = "/* $sectionTitle */ ";
                                        }
                $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.
                $wgOut->addHTML( '</div>' );
  
                if ( $this->formtype == 'diff' ) {
 -                      $this->showDiff();
 +                      try {
 +                              $this->showDiff();
 +                      } catch ( MWContentSerializationException $ex ) {
 +                              $msg = wfMessage( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                              $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>');
 +                      }
                }
        }
  
                        $oldtext = $this->mTitle->getDefaultMessageText();
                        if( $oldtext !== false ) {
                                $oldtitlemsg = 'defaultmessagetext';
 +                              $oldContent = 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 );
  
 -              wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
 +              $textboxContent = ContentHandler::makeContent( $this->textbox1, $this->getTitle(),
 +                                                                                                              $this->content_model, $this->content_format );
 +
 +              $newContent = $this->mArticle->replaceSectionContent(
 +                                                                                      $this->section, $textboxContent,
 +                                                                                      $this->summary, $this->edittime );
 +
 +              ContentHandler::runLegacyHooks( 'EditPageGetDiffText', array( $this, &$newContent ) );
 +              wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) );
  
                $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
 -              $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts );
 +              $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
  
 -              if ( $oldtext !== false  || $newtext != '' ) {
 +              if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
                        $oldtitle = wfMessage( $oldtitlemsg )->parse();
                        $newtitle = wfMessage( 'yourtext' )->parse();
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $oldtext, $newtext );
 +                      $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $oldContent, $newContent );
 +
                        $difftext = $de->getDiff( $oldtitle, $newtitle );
                        $de->showDiffStyle();
                } else {
                // Allow for site and per-namespace customization of contribution/copyright notice.
                wfRunHooks( 'EditPageCopyrightWarning', array( $title, &$copywarnMsg ) );
  
-               $msg = call_user_func_array( "wfMessage", $copywarnMsg );
                return "<div id=\"editpage-copywarn\">\n" .
-                       $msg->plain() . "\n</div>";
+                       call_user_func_array( 'wfMessage', $copywarnMsg )->plain() . "\n</div>";
        }
  
        protected function showStandardInputs( &$tabindex = 2 ) {
                $edithelpurl = Skin::makeInternalOrExternalUrl( wfMessage( 'edithelppage' )->inContentLanguage()->text() );
                $edithelp = '<a target="helpwindow" href="' . $edithelpurl . '">' .
                        wfMessage( 'edithelp' )->escaped() . '</a> ' .
-                       wfMessage( 'newwindow' )->escaped();
+                       wfMessage( 'newwindow' )->parse();
                $wgOut->addHTML( "      <span class='cancelLink'>{$cancel}</span>\n" );
                $wgOut->addHTML( "      <span class='editHelp'>{$edithelp}</span>\n" );
                $wgOut->addHTML( "</div><!-- editButtons -->\n</div><!-- editOptions -->\n" );
                if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $this->textbox2, $this->textbox1 );
 -                      $de->showDiff(
 +                      $content1 = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 +                      $content2 = ContentHandler::makeContent( $this->textbox2, $this->getTitle(), $this->content_model, $this->content_format );
 +
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +                      $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $content2, $content1 );
 +                      $de->showDiff( 
                                wfMessage( 'yourtext' )->parse(),
                                wfMessage( 'storedversion' )->text()
                        );
                        return $parsedNote;
                }
  
 -              if ( $this->mTriedSave && !$this->mTokenOk ) {
 -                      if ( $this->mTokenOkExceptSuffix ) {
 -                              $note = wfMessage( 'token_suffix_mismatch' )->plain();
 -                      } else {
 -                              $note = wfMessage( 'session_fail_preview' )->plain();
 -                      }
 -              } elseif ( $this->incompleteForm ) {
 -                      $note = wfMessage( 'edit_form_incomplete' )->plain();
 -              } else {
 -                      $note = wfMessage( 'previewnote' )->plain() .
 -                              ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]';
 -              }
 +              $note = '';
  
 -              $parserOptions = 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;
 -                      }
 -
 -                      # Used messages to make sure grep find them:
 -                      # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 -                      $class = 'mw-code';
 -                      if ( $level ) {
 -                              if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMessage( "{$level}csspreview" )->text() . "\n</div>";
 -                                      $class .= " mw-css";
 -                              } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMessage( "{$level}jspreview" )->text() . "\n</div>";
 -                                      $class .= " mw-js";
 +              try {
 +                      $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(),
 +                                                                                                      $this->content_model, $this->content_format );
 +
 +                      if ( $this->mTriedSave && !$this->mTokenOk ) {
 +                              if ( $this->mTokenOkExceptSuffix ) {
 +                                      $note = wfMessage( 'token_suffix_mismatch' )->plain() ;
                                } else {
 -                                      throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' );
 +                                      $note = wfMessage( 'session_fail_preview' )->plain() ;
                                }
 -                              $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
 -                              $previewHTML = $parserOutput->getText();
 +                      } elseif ( $this->incompleteForm ) {
 +                              $note = wfMessage( 'edit_form_incomplete' )->plain() ;
                        } else {
 -                              $previewHTML = '';
 -                      }
 +                              $note = wfMessage( 'previewnote' )->plain() .
 +                                      ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]';
 +                      }
 +
 +                      $parserOptions = 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->isCssOrJsPage() ) {
 +                              if( $this->mTitle->isCssJsSubpage() ) {
 +                                      $level = 'user';
 +                              } elseif( $this->mTitle->isCssOrJsPage() ) {
 +                                      $level = 'site';
 +                              } else {
 +                                      $level = false;
 +                              }
  
 -                      $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n";
 -              } else {
 -                      $toparse = $this->textbox1;
 +                              if ( $content->getModel() == CONTENT_MODEL_CSS ) {
 +                                      $format = 'css';
 +                              } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
 +                                      $format = 'js';
 +                              } else {
 +                                      $format = false;
 +                              }
  
 -                      # If we're adding a comment, we need to show the
 -                      # summary as the headline
 -                      if ( $this->section == "new" && $this->summary != "" ) {
 -                              $toparse = wfMessage( 'newsectionheaderdefaultlevel', $this->summary )->inContentLanguage()->text() . "\n\n" . $toparse;
 +                              # Used messages to make sure grep find them:
 +                              # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 +                              if( $level && $format ) {
 +                                      $note = "<div id='mw-{$level}{$format}preview'>" . wfMessage( "{$level}{$format}preview" )->text()  . "</div>";
 +                              } else {
 +                                      $note = wfMessage( 'previewnote' )->text() ;
 +                              }
 +                      } else {
 +                              $note = wfMessage( 'previewnote' )->text() ;
                        }
  
 -                      wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
 +                      $rt = $content->getRedirectChain();
  
 -                      $parserOptions->enableLimitReport();
 -
 -                      $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
 -                      $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
 -
 -                      $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 );
 +                              }
 +
 +                              $hook_args = array( $this, &$content );
 +                              ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
 +                              wfRunHooks( 'EditPageGetPreviewContent', $hook_args );
 +
 +                              $parserOptions->enableLimitReport();
 +
 +                              # For CSS/JS pages, we should have called the ShowRawCssJs hook here.
 +                              # But it's now deprecated, so never mind
  
 -                      if ( count( $parserOutput->getWarnings() ) ) {
 -                              $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                              $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
 +                              $parserOutput = $content->getParserOutput( $this->getArticle()->getTitle(), null, $parserOptions );
 +
 +                              $previewHTML = $parserOutput->getText();
 +                              $this->mParserOutput = $parserOutput;
 +                              $wgOut->addParserOutputNoText( $parserOutput );
 +
 +                              if ( count( $parserOutput->getWarnings() ) ) {
 +                                      $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                              }
                        }
 +              } catch (MWContentSerializationException $ex) {
 +                      $m = wfMessage('content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $note .= "\n\n" . $m->parse();
 +                      $previewHTML = '';
                }
  
                if ( $this->isConflict ) {
@@@ -391,7 -391,7 +391,7 @@@ function wfArrayToCgi( $array1, $array
  
        $cgi = '';
        foreach ( $array1 as $key => $value ) {
 -              if ( !is_null($value) && $value !== false ) {
 +              if ( $value !== false ) {
                        if ( $cgi != '' ) {
                                $cgi .= '&';
                        }
                        } else {
                                if ( is_object( $value ) ) {
                                        $value = $value->__toString();
 +                              } elseif( !is_null( $value ) ) {
 +                                      $cgi .= urlencode( $key ) . '=' . urlencode( $value );
 +                              } else {
 +                                      $cgi .= urlencode( $key );
                                }
 -                              $cgi .= urlencode( $key ) . '=' . urlencode( $value );
                        }
                }
        }
@@@ -443,15 -440,14 +443,15 @@@ function wfCgiToArray( $query ) 
                        continue;
                }
                if ( strpos( $bit, '=' ) === false ) {
 -                      // Pieces like &qwerty become 'qwerty' => '' (at least this is what php does)
 -                      $key = $bit;
 -                      $value = '';
 +                      // Pieces like &qwerty become 'qwerty' => null
 +                      $key = urldecode( $bit );
 +                      $value = null;
                } else {
                        list( $key, $value ) = explode( '=', $bit );
 +                      $key = urldecode( $key );
 +                      $value = urldecode( $value );
                }
 -              $key = urldecode( $key );
 -              $value = urldecode( $value );
 +
                if ( strpos( $key, '[' ) !== false ) {
                        $keys = array_reverse( explode( '[', $key ) );
                        $key = array_pop( $keys );
   * Append a query string to an existing URL, which may or may not already
   * have query string parameters already. If so, they will be combined.
   *
 + * @deprecated in 1.20. Use Uri class.
   * @param $url String
   * @param $query Mixed: string or associative array
   * @return string
   */
  function wfAppendQuery( $url, $query ) {
 -      if ( is_array( $query ) ) {
 -              $query = wfArrayToCgi( $query );
 -      }
 -      if( $query != '' ) {
 -              if( false === strpos( $url, '?' ) ) {
 -                      $url .= '?';
 -              } else {
 -                      $url .= '&';
 -              }
 -              $url .= $query;
 -      }
 -      return $url;
 +      $obj = new Uri( $url );
 +      $obj->extendQuery( $query );
 +      return $obj->toString();
  }
  
  /**
@@@ -572,13 -576,49 +572,13 @@@ function wfExpandUrl( $url, $defaultPro
   * @todo Need to integrate this into wfExpandUrl (bug 32168)
   *
   * @since 1.19
 + * @deprecated
   * @param $urlParts Array URL parts, as output from wfParseUrl
   * @return string URL assembled from its component parts
   */
  function wfAssembleUrl( $urlParts ) {
 -      $result = '';
 -
 -      if ( isset( $urlParts['delimiter'] ) ) {
 -              if ( isset( $urlParts['scheme'] ) ) {
 -                      $result .= $urlParts['scheme'];
 -              }
 -
 -              $result .= $urlParts['delimiter'];
 -      }
 -
 -      if ( isset( $urlParts['host'] ) ) {
 -              if ( isset( $urlParts['user'] ) ) {
 -                      $result .= $urlParts['user'];
 -                      if ( isset( $urlParts['pass'] ) ) {
 -                              $result .= ':' . $urlParts['pass'];
 -                      }
 -                      $result .= '@';
 -              }
 -
 -              $result .= $urlParts['host'];
 -
 -              if ( isset( $urlParts['port'] ) ) {
 -                      $result .= ':' . $urlParts['port'];
 -              }
 -      }
 -
 -      if ( isset( $urlParts['path'] ) ) {
 -              $result .= $urlParts['path'];
 -      }
 -
 -      if ( isset( $urlParts['query'] ) ) {
 -              $result .= '?' . $urlParts['query'];
 -      }
 -
 -      if ( isset( $urlParts['fragment'] ) ) {
 -              $result .= '#' . $urlParts['fragment'];
 -      }
 -
 -      return $result;
 +      $obj = new Uri( $urlParts );
 +      return $obj->toString();
  }
  
  /**
@@@ -725,7 -765,6 +725,7 @@@ function wfUrlProtocolsWithoutProtRel(
   * 2) Handles protocols that don't use :// (e.g., mailto: and news: , as well as protocol-relative URLs) correctly
   * 3) Adds a "delimiter" element to the array, either '://', ':' or '//' (see (2))
   *
 + * @deprecated
   * @param $url String: a URL to parse
   * @return Array: bits of the URL in an associative array, per PHP docs
   */
@@@ -1025,7 -1064,6 +1025,7 @@@ function wfLogDBError( $text ) 
                } else {
                        $d = date_create( "now", $logDBErrorTimeZoneObject );
                }
 +              $date = $d->format( 'D M j G:i:s T Y' );
  
                $date = $d->format( 'D M j G:i:s T Y' );
  
@@@ -1504,8 -1542,6 +1504,6 @@@ function wfMsgGetKey( $key, $useDB = tr
  /**
   * Replace message parameter keys on the given formatted output.
   *
-  * @deprecated since 1.18
-  *
   * @param $message String
   * @param $args Array
   * @return string
@@@ -2366,7 -2402,6 +2364,7 @@@ define( 'TS_ISO_8601_BASIC', 9 )
  /**
   * Get a timestamp string in one of various formats
   *
 + * @deprecated
   * @param $outputtype Mixed: A timestamp in one of the supported formats, the
   *                    function will autodetect which format is supplied and act
   *                    accordingly.
diff --combined includes/ImagePage.php
@@@ -94,47 -94,10 +94,10 @@@ class ImagePage extends Article 
        /**
         * Handler for action=render
         * Include body text only; none of the image extras
-        * However, also include the shared description text
-        * so that cascading ForeignAPIRepo's work.
-        *
-        * @note This uses a div with the class "mw-shared-image-desc"
-        *    as opposed to the id "mw-shared-image-desc" since the text
-        *    from here may be cascadingly transcluded to other shared
-        *    repos, and we want all ids to be unique. On normal
-        *    view, the outermost shared description will still have
-        *    the id.
-        *
-        * This also differs from normal view in that "shareddescriptionfollows"
-        * message is not shown. I was not sure if it was appropriate to
-        * add that message here.
         */
        public function render() {
-               $out = $this->getContext()->getOutput();
-                 $this->loadFile();
-                 $descText = $this->mPage->getFile()->getDescriptionText();
-               $out->setArticleBodyOnly( true );
-               if ( !$descText ) {
-                       // If no description text, just do standard action=render
-                       parent::view();
-               } else {
-                       if ( $this->mPage->getID() !== 0 ) {
-                               // Local description exists. We need to output both
-                               parent::view();
-                               $out->addHTML( '<div class="mw-shared-image-desc">' . $descText . "</div>\n" );
-                       } else {
-                               // We don't want to output both a "noarticletext" message and the shared
-                               // description, so don't call parent::view().
-                               $out->addHTML( '<div class="mw-shared-image-desc">' . $descText . "</div>\n" );
-                               // Since we did not call parent::view(), have to call some methods it
-                               // normally takes care of. (Not that it matters much since skin not displayed)
-                               $out->setArticleFlag( true );
-                               $out->setPageTitle( $this->getTitle()->getPrefixedText() );
-                               $this->mPage->doViewUpdates( $this->getContext()->getUser() );
-                       }
-               }
+               $this->getContext()->getOutput()->setArticleBodyOnly( true );
+               parent::view();
        }
  
        public function view() {
                        $out->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content',
                                'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(),
                                'class' => 'mw-content-'.$pageLang->getDir() ) ) );
 +
                        parent::view();
 +
                        $out->addHTML( Xml::closeElement( 'div' ) );
                } else {
                        # Just need to set the right headers
                        if ( !$fol->isDisabled() ) {
                                $out->addWikiText( $fol->plain() );
                        }
-                       $out->addHTML( '<div id="shared-image-desc" class="mw-shared-image-desc">' . $this->mExtraDescription . "</div>\n" );
+                       $out->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . "</div>\n" );
                }
  
                $this->closeShowImage();
        }
  
        /**
 -       * Overloading Article's getContent method.
 +       * Overloading Article's getContentObject method.
         *
         * Omit noarticletext if sharedupload; text will be fetched from the
         * shared upload server if possible.
         * @return string
         */
 -      public function getContent() {
 +      public function getContentObject() {
                $this->loadFile();
                if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) {
 -                      return '';
 +                      return null;
                }
 -              return parent::getContent();
 +              return parent::getContentObject();
        }
  
        protected function openShowImage() {
diff --combined includes/LinksUpdate.php
@@@ -39,6 -39,8 +39,8 @@@ class LinksUpdate extends SqlDataUpdat
                $mCategories,    //!< Map of category names to sort keys
                $mInterlangs,    //!< Map of language codes to titles
                $mProperties,    //!< Map of arbitrary name to value
+               $mDb,            //!< Database connection reference
+               $mOptions,       //!< SELECT options to be used (array)
                $mRecursive;     //!< Whether to queue jobs for recursive updates
  
        /**
@@@ -69,7 -71,6 +71,7 @@@
                }
  
                $this->mParserOutput = $parserOutput;
 +
                $this->mLinks = $parserOutput->getLinks();
                $this->mImages = $parserOutput->getImages();
                $this->mTemplates = $parserOutput->getTemplates();
   **/
  class LinksDeletionUpdate extends SqlDataUpdate {
  
-       protected $mTitle;     //!< Title the title of page that was deleted
+       protected $mPage;     //!< WikiPage the wikipage that was deleted
  
        /**
         * Constructor
         *
         * @param $page WikiPage Page we are updating
         */
-       function __construct( Title $title ) {
+       function __construct( WikiPage $page ) {
                parent::__construct( false ); // no implicit transaction
  
-               $this->mTitle = $title;
+               $this->mPage = $page;
 +
-               if ( !$title->getArticleID() ) {
-                       throw new MWException( "The Title object did not provide an article ID. Perhaps the page doesn't exist?" );
++              if ( !$page->getId() ) {
++                      throw new MWException( "Page ID not known, perhaps the page doesn't exist?" );
 +              }
        }
  
        /**
         * Do some database updates after deletion
         */
        public function doUpdate() {
-               $title = $this->mTitle;
-               $id = $title->getArticleID();
+               $title = $this->mPage->getTitle();
+               $id = $this->mPage->getId();
  
                # Delete restrictions for it
                $this->mDb->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
                        $cats [] = $row->cl_to;
                }
  
-               $this->updateCategoryCounts( array(), $cats );
+               $this->mPage->updateCategoryCounts( array(), $cats );
  
                # If using cascading deletes, we can skip some explicit deletes
                if ( !$this->mDb->cascadingDeletes() ) {
                                __METHOD__ );
                }
        }
 +
 +      /**
 +       * Update all the appropriate counts in the category table.
 +       * @param $added array associative array of category name => sort key
 +       * @param $deleted array associative array of category name => sort key
 +       */
 +      function updateCategoryCounts( $added, $deleted ) {
 +              $a = WikiPage::factory( $this->mTitle );
 +              $a->updateCategoryCounts(
 +                      array_keys( $added ), array_keys( $deleted )
 +              );
 +      }
  }
diff --combined includes/OutputPage.php
@@@ -1990,12 -1990,11 +1990,15 @@@ class OutputPage extends ContextSource 
                        wfProfileOut( 'Output-skin' );
                }
  
+               // This hook allows last minute changes to final overall output by modifying output buffer
+               wfRunHooks( 'AfterFinalPageOutput', array( $this ) );
                $this->sendCacheControl();
 +
 +              wfRunHooks( 'AfterFinalPageOutput', array( &$this ) );
 +
                ob_end_flush();
 +
                wfProfileOut( __METHOD__ );
        }
  
@@@ -3549,7 -3548,7 +3552,7 @@@ $template
         *
         * Is equivalent to:
         *
-        *    $wgOut->addWikiText( "<div class='error'>\n" . wfMsgNoTrans( 'some-error' ) . "\n</div>" );
+        *    $wgOut->addWikiText( "<div class='error'>\n" . wfMessage( 'some-error' )->plain() . "\n</div>" );
         *
         * The newline after opening div is needed in some wikitext. See bug 19226.
         *
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;
@@@ -85,7 -84,6 +85,6 @@@
        var $mLength = -1;                // /< The page length, 0 for special pages
        var $mRedirect = null;            // /< Is the article at this title a redirect?
        var $mNotificationTimestamp = array(); // /< Associative array of user ID -> timestamp/false
-       var $mBacklinkCache = null;       // /< Cache of links to this title
        var $mHasSubpage;                 // /< Whether a page has any subpages
        // @}
  
                }
        }
  
 +      /**
 +       * Returns a list of fields that are to be selected for initializing Title objects or LinkCache entries.
 +       * Uses $wgContentHandlerUseDB to determine whether to include page_content_model.
 +       *
 +       * @return array
 +       */
 +      protected static function getSelectFields() {
 +              global $wgContentHandlerUseDB;
 +
 +              $fields = array(
 +                      'page_namespace', 'page_title', 'page_id',
 +                      'page_len', 'page_is_redirect', 'page_latest',
 +              );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'page_content_model';
 +              }
 +
 +              return $fields;
 +      }
 +
        /**
         * Create a new Title from an article ID
         *
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                $row = $db->selectRow(
                        'page',
 -                      array(
 -                              'page_namespace', 'page_title', 'page_id',
 -                              'page_len', 'page_is_redirect', 'page_latest',
 -                      ),
 +                      self::getSelectFields(),
                        array( 'page_id' => $id ),
                        __METHOD__
                );
  
                $res = $dbr->select(
                        'page',
 -                      array(
 -                              'page_namespace', 'page_title', 'page_id',
 -                              'page_len', 'page_is_redirect', 'page_latest',
 -                      ),
 +                      self::getSelectFields(),
                        array( 'page_id' => $ids ),
                        __METHOD__
                );
                                $this->mRedirect = (bool)$row->page_is_redirect;
                        if ( isset( $row->page_latest ) )
                                $this->mLatestID = (int)$row->page_latest;
 +                      if ( isset( $row->page_content_model ) )
 +                              $this->mContentModel = strval( $row->page_content_model );
 +                      else
 +                              $this->mContentModel = false; # initialized lazily in getContentModel()
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
 +                      $this->mContentModel = false; # initialized lazily in getContentModel()
                }
        }
  
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = str_replace( '_', ' ', $title );
 +              $t->mContentModel = false; # initialized lazily in getContentModel()
                return $t;
        }
  
         *
         * @param $text String: Text with possible redirect
         * @return Title: The corresponding Title
 +       * @deprecated since 1.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 String: Content model id
 +       */
 +      public function getContentModel() {
 +              if ( !$this->mContentModel ) {
 +                      $linkCache = LinkCache::singleton();
 +                      $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
 +              }
 +
 +              if ( !$this->mContentModel ) {
 +                      $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
 +              }
 +
 +              if( !$this->mContentModel ) {
 +                      throw new MWException( "failed to determin content model!" );
 +              }
 +
 +              return $this->mContentModel;
 +      }
 +
 +      /**
 +       * Convenience method for checking a title's content model name
 +       *
 +       * @param int $id
 +       * @return Boolean true if $this->getContentModel() == $id
 +       */
 +      public function hasContentModel( $id ) {
 +              return $this->getContentModel() == $id;
 +      }
 +
        /**
         * Get the namespace text
         *
         * @return Bool
         */
        public function 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 ) ? strval( $row->page_content_model ) : false;
                if ( !$this->mRedirect ) {
                        return false;
                }
                if( !is_object( $rev ) ){
                        return false;
                }
 -              $text = $rev->getText();
 +              $content = $rev->getContent();
                # Does the redirect point to the source?
                # Or is it a broken self-redirect, usually caused by namespace collisions?
 -              $m = array();
 -              if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) {
 -                      $redirTitle = Title::newFromText( $m[1] );
 -                      if ( !is_object( $redirTitle ) ||
 -                              ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
 -                              $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) {
 +              $redirTitle = $content->getRedirectTarget();
 +
 +              if ( $redirTitle ) {
 +                      if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
 +                              $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
                                wfDebug( __METHOD__ . ": redirect points to other page\n" );
                                return false;
 +                      } else {
 +                              return true;
                        }
                } else {
 -                      # Fail safe
 -                      wfDebug( __METHOD__ . ": failsafe\n" );
 +                      # Fail safe (not a redirect after all. strange.)
 +                      wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
 +                                              " is a redirect, but it doesn't contain a valid redirect.\n" );
                        return false;
                }
 -              return true;
        }
  
        /**
                        'rd_title' => $this->getDBkey(),
                        'rd_from = page_id'
                );
+               if ( $this->isExternal() ) {
+                       $where['rd_interwiki'] = $this->getInterwiki();
+               } else {
+                       $where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL';
+               }
                if ( !is_null( $ns ) ) {
                        $where['page_namespace'] = $ns;
                }
         *
         * @return BacklinkCache
         */
-       function getBacklinkCache() {
-               if ( is_null( $this->mBacklinkCache ) ) {
-                       $this->mBacklinkCache = new BacklinkCache( $this );
-               }
-               return $this->mBacklinkCache;
+       public function getBacklinkCache() {
+               return BacklinkCache::get( $this );
        }
  
        /**
                if ( $this->isSpecialPage() ) {
                        // special pages are in the user language
                        return $wgLang;
 -              } elseif ( $this->isCssOrJsPage() || $this->isCssJsSubpage() ) {
 -                      // css/js should always be LTR and is, in fact, English
 -                      return wfGetLangObj( 'en' );
 -              } elseif ( $this->getNamespace() == NS_MEDIAWIKI ) {
 -                      // Parse mediawiki messages with correct target language
 -                      list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $this->getText() );
 -                      return wfGetLangObj( $lang );
                }
 -              global $wgContLang;
 -              // If nothing special, it should be in the wiki content language
 -              $pageLang = $wgContLang;
 +
 +              //TODO: use the LinkCache to cache this!
 +              //NOTE: ContentHandler::getPageLanguage() may need to load the content to determine the page language!
 +              $contentHandler = ContentHandler::getForTitle( $this );
 +              $pageLang = $contentHandler->getPageLanguage( $this );
 +
                // Hook at the end because we don't want to override the above stuff
                wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) );
                return wfGetLangObj( $pageLang );
         * @return Language
         */
        public function getPageViewLanguage() {
 -              $pageLang = $this->getPageLanguage();
 -              // If this is nothing special (so the content is converted when viewed)
 -              if ( !$this->isSpecialPage()
 -                      && !$this->isCssOrJsPage() && !$this->isCssJsSubpage()
 -                      && $this->getNamespace() !== NS_MEDIAWIKI
 -              ) {
 +              global $wgLang;
 +
 +              if ( $this->isSpecialPage() ) {
                        // If the user chooses a variant, the content is actually
                        // in a language whose code is the variant code.
 -                      $variant = $pageLang->getPreferredVariant();
 -                      if ( $pageLang->getCode() !== $variant ) {
 -                              $pageLang = Language::factory( $variant );
 +                      $variant = $wgLang->getPreferredVariant();
 +                      if ( $wgLang->getCode() !== $variant ) {
 +                              return Language::factory( $variant );
                        }
 +
 +                      return $wgLang;
                }
 +
 +              //NOTE: can't be cached, depends on user settings
 +              //NOTE: ContentHandler::getPageViewLanguage() may need to load the content to determine the page language!
 +              $contentHandler = ContentHandler::getForTitle( $this );
 +              $pageLang = $contentHandler->getPageViewLanguage( $this );
                return $pageLang;
        }
  }
diff --combined includes/WikiPage.php
@@@ -187,21 -187,7 +187,21 @@@ class WikiPage extends Page implements 
         * @return Array
         */
        public function getActionOverrides() {
 -              return array();
 +              $content_handler = $this->getContentHandler();
 +              return $content_handler->getActionOverrides();
 +      }
 +
 +      /**
 +       * Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
 +       *
 +       * Shorthand for ContentHandler::getForModelID( $this->getContentModel() );
 +       *
 +       * @return ContentHandler
 +       *
 +       * @since 1.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 String
 +       *
 +       * @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 {
 +                              $title = $this->mTitle->getPrefixedDBkey();
 +                              wfWarn( "Page $title exists but has no (visible) revisions!" );
 +                      }
                }
 +
 +              # use the default model for this page
 +              return $this->mTitle->getContentModel();
        }
  
        /**
                return null;
        }
  
 +      /**
 +       * 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__ );
                return $ret;
        }
  
 +    /**
 +     * Get the content that needs to be saved in order to undo all revisions
 +     * between $undo and $undoafter. Revisions must belong to the same page,
 +     * must exist and must not be deleted
 +     * @param $undo Revision
 +     * @param $undoafter Revision Must be an earlier revision than $undo
 +     * @return mixed string on success, false on failure
 +     * @since 1.WD
 +     * Before we had the Content object, this was done in getUndoText
 +     */
 +    public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
 +        $handler = $undo->getContentHandler();
 +        return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
 +    }
 +
        /**
         * Get the text that needs to be saved in order to undo all revisions
         * between $undo and $undoafter. Revisions must belong to the same page,
         * @param $undo Revision
         * @param $undoafter Revision Must be an earlier revision than $undo
         * @return mixed string on success, false on failure
 +       * @deprecated since 1.WD: use ContentHandler::getUndoContent() instead.
         */
        public function getUndoText( Revision $undo, Revision $undoafter = null ) {
 -              $cur_text = $this->getRawText();
 -              if ( $cur_text === false ) {
 -                      return false; // no page
 -              }
 -              $undo_text = $undo->getText();
 -              $undoafter_text = $undoafter->getText();
 +              wfDeprecated( __METHOD__, '1.WD' );
  
 -              if ( $cur_text == $undo_text ) {
 -                      # No use doing a merge if it's just a straight revert.
 -                      return $undoafter_text;
 -              }
 +              $this->loadLastEdit();
  
 -              $undone_text = '';
 +              if ( $this->mLastRevision ) {
 +                      if ( is_null( $undoafter ) ) {
 +                              $undoafter = $undo->getPrevious();
 +                      }
  
 -              if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) {
 -                      return false;
 +                      $handler = $this->getContentHandler();
 +                      $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter );
 +
 +                      if ( !$undone ) {
 +                              return false;
 +                      } else {
 +                              return ContentHandler::getContentText( $undone );
 +                      }
                }
  
 -              return $undone_text;
 +              return false;
        }
  
        /**
         * @param $text String: new text of the section
         * @param $sectionTitle String: new section's subject, only if $section is 'new'
         * @param $edittime String: revision timestamp or null to use the current revision
 -       * @return string Complete article text, or null if error
 +       * @return String new complete article text, or null if error
 +       *
 +       * @deprecated since 1.WD, use replaceSectionContent() instead
         */
        public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) {
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              if ( strval( $section ) == '' ) { //NOTE: keep condition in sync with condition in replaceSectionContent!
 +                      // Whole-page edit; let the whole text through
 +                      return $text;
 +              }
 +
 +              if ( !$this->supportsSections() ) {
 +                      throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() );
 +              }
 +
 +              # could even make section title, but that's not required.
 +              $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() );
 +
 +              $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime );
 +
 +              return ContentHandler::getContentText( $newContent );
 +      }
 +
 +      /**
 +       * Returns true iff this page's content model supports sections.
 +       *
 +       * @return boolean whether sections are supported.
 +       *
 +       * @todo: the skin should check this and not offer section functionality if sections are not supported.
 +       * @todo: the EditPage should check this and not offer section functionality if sections are not supported.
 +       */
 +      public function supportsSections() {
 +              return $this->getContentHandler()->supportsSections();
 +      }
 +
 +      /**
 +       * @param $section null|bool|int or a section number (0, 1, 2, T1, T2...)
 +       * @param $content Content: new content of the section
 +       * @param $sectionTitle String: new section's subject, only if $section is 'new'
 +       * @param $edittime String: revision timestamp or null to use the current revision
 +       *
 +       * @return Content new complete article content, or null if error
 +       *
 +       * @since 1.WD
 +       */
 +      public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) {
                wfProfileIn( __METHOD__ );
  
                if ( strval( $section ) == '' ) {
                        // Whole-page edit; let the whole text through
 +                      $newContent = $sectionContent;
                } else {
 +                      if ( !$this->supportsSections() ) {
 +                              throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() );
 +                      }
 +
                        // Bug 30711: always use current version when adding a new section
                        if ( is_null( $edittime ) || $section == 'new' ) {
 -                              $oldtext = $this->getRawText();
 -                              if ( $oldtext === false ) {
 +                              $oldContent = $this->getContent();
 +                              if ( ! $oldContent ) {
                                        wfDebug( __METHOD__ . ": no page text\n" );
                                        wfProfileOut( __METHOD__ );
                                        return null;
                                        return null;
                                }
  
 -                              $oldtext = $rev->getText();
 +                              $oldContent = $rev->getContent();
                        }
  
 -                      if ( $section == 'new' ) {
 -                              # Inserting a new section
 -                              $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' )
 -                                      ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : '';
 -                              if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
 -                                      $text = strlen( trim( $oldtext ) ) > 0
 -                                              ? "{$oldtext}\n\n{$subject}{$text}"
 -                                              : "{$subject}{$text}";
 -                              }
 -                      } else {
 -                              # Replacing an existing section; roll out the big guns
 -                              global $wgParser;
 -
 -                              $text = $wgParser->replaceSection( $oldtext, $section, $text );
 -                      }
 +                      $newContent = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle );
                }
  
                wfProfileOut( __METHOD__ );
 -              return $text;
 +              return $newContent;
        }
  
        /**
         *     revision:                The revision object for the inserted revision, or null
         *
         *  Compatibility note: this function previously returned a boolean value indicating success/failure
 +       *
 +       * @deprecated since 1.WD: use doEditContent() instead.
 +       * @todo: use doEditContent() instead everywhere
         */
        public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
 +              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, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol;
  
                # 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" );
 +              # handle hook
 +              $hook_args = array( &$this, &$user, &$content, &$summary,
 +                                                      $flags & EDIT_MINOR, null, null, &$flags, &$status );
 +
 +              if ( !wfRunHooks( 'ArticleContentSave', $hook_args )
 +                      || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) {
 +
 +                      wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" );
  
                        if ( $status->isOK() ) {
                                $status->fatal( 'edit-hook-aborted' );
                $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' );
                $bot = $flags & EDIT_FORCE_BOT;
  
 -              $oldtext = $this->getRawText(); // current revision
 -              $oldsize = strlen( $oldtext );
 +              $old_content = $this->getContent( Revision::RAW ); // current revision's content
 +
 +              $oldsize = $old_content ? $old_content->getSize() : 0;
                $oldid = $this->getLatest();
                $oldIsRedirect = $this->isRedirect();
                $oldcountable = $this->isCountable();
  
 +              $handler = $content->getContentHandler();
 +
                # Provide autosummaries if one is not provided and autosummaries are enabled.
                if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
 -                      $summary = self::getAutosummary( $oldtext, $text, $flags );
 +                      if ( !$old_content ) $old_content = null;
 +                      $summary = $handler->getAutosummary( $old_content, $content, $flags );
                }
  
 -              $editInfo = $this->prepareTextForEdit( $text, null, $user );
 -              $text = $editInfo->pst;
 -              $newsize = strlen( $text );
 +              $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format );
 +              $serialized = $editInfo->pst;
 +              $content = $editInfo->pstContent;
 +              $newsize =  $content->getSize();
  
                $dbw = wfGetDB( DB_MASTER );
                $now = wfTimestampNow();
  
                                wfProfileOut( __METHOD__ );
                                return $status;
 -                      } elseif ( $oldtext === false ) {
 +                      } elseif ( !$old_content ) {
                                # Sanity check for bug 37225
                                wfProfileOut( __METHOD__ );
                                throw new MWException( "Could not find text for current revision {$oldid}." );
                                'page'       => $this->getId(),
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
 -                              'text'       => $text,
 +                              'text'       => $serialized,
 +                              'len'        => $newsize,
                                'parent_id'  => $oldid,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
 -                              'timestamp'  => $now
 -                      ) );
 -                      # Bug 37225: use accessor to get the text as Revision may trim it.
 -                      # After trimming, the text may be a duplicate of the current text.
 -                      $text = $revision->getText(); // sanity; EditPage should trim already
 +                              'timestamp'  => $now,
 +                              'content_model' => $content->getModel(),
 +                              'content_format' => $serialisation_format,
 +                      ) ); #XXX: pass content object?!
  
 -                      $changed = ( strcmp( $text, $oldtext ) != 0 );
 +                      $changed = !$content->equals( $old_content );
  
                        if ( $changed ) {
 +                              if ( !$content->isValid() ) {
 +                                      throw new MWException( "New content failed validity check!" );
 +                              }
 +
                                $dbw->begin( __METHOD__ );
 +
 +                              $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
 +                              $status->merge( $prepStatus );
 +
 +                              if ( !$status->isOK() ) {
 +                                      $dbw->rollback();
 +
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
 +
                                $revisionId = $revision->insertOn( $dbw );
  
                                # Update page
                        }
  
                        # Update links tables, site stats, etc.
 -                      $this->doEditUpdates( $revision, $user, array( 'changed' => $changed,
 -                              'oldcountable' => $oldcountable ) );
 +                      $this->doEditUpdates(
 +                              $revision,
 +                              $user,
 +                              array(
 +                                      'changed' => $changed,
 +                                      'oldcountable' => $oldcountable
 +                              )
 +                      );
  
                        if ( !$changed ) {
                                $status->warning( 'edit-no-change' );
  
                        $dbw->begin( __METHOD__ );
  
 +                      $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
 +                      $status->merge( $prepStatus );
 +
 +                      if ( !$status->isOK() ) {
 +                              $dbw->rollback();
 +
 +                              wfProfileOut( __METHOD__ );
 +                              return $status;
 +                      }
 +
 +                      $status->merge( $prepStatus );
 +
                        # Add the page record; stake our claim on this title!
                        # This will return false if the article already exists
                        $newid = $this->insertOn( $dbw );
                                'page'       => $newid,
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
 -                              'text'       => $text,
 +                              'text'       => $serialized,
 +                              'len'        => $newsize,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
 -                              'timestamp'  => $now
 +                              'timestamp'  => $now,
 +                              'content_model' => $content->getModel(),
 +                              'content_format' => $serialisation_format,
                        ) );
                        $revisionId = $revision->insertOn( $dbw );
  
                        # Bug 37225: use accessor to get the text as Revision may trim it
 -                      $text = $revision->getText(); // sanity; EditPage should trim already
 +                      $content = $revision->getContent(); // sanity; get normalized version
  
                        # Update the page record with revision data
                        $this->updateRevisionOn( $dbw, $revision, 0 );
                                        $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
                                # Add RC row to the DB
                                $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
 -                                      '', strlen( $text ), $revisionId, $patrolled );
 +                                      '', $content->getSize(), $revisionId, $patrolled );
  
                                # Log auto-patrolled edits
                                if ( $patrolled ) {
                        # Update links, etc.
                        $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
  
 -                      wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
 -                              $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
 +                      $hook_args = array( &$this, &$user, $content, $summary,
 +                                                              $flags & EDIT_MINOR, null, null, &$flags, $revision );
 +
 +                      ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args );
 +                      wfRunHooks( 'ArticleContentInsertComplete', $hook_args );
                }
  
                # Do updates right now unless deferral was requested
                // Return the new revision (or null) to the caller
                $status->value['revision'] = $revision;
  
 -              wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
 -                      $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
 +              $hook_args = array( &$this, &$user, $content, $summary,
 +                                                      $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId );
 +
 +              ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args );
 +              wfRunHooks( 'ArticleContentSaveComplete', $hook_args );
  
                # Promote user to any groups they meet the criteria for
                $user->addAutopromoteOnceGroups( 'onEdit' );
        /**
         * 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 );
 +              $updates = $content->getSecondaryDataUpdates( $this->getTitle(), null, true, $editInfo->output );
                DataUpdate::runUpdates( $updates );
  
                wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
                }
  
                DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) );
 -              DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) );
 +              DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content->getTextForSearchIndex() ) );
 +              #@TODO: let the search engine decide what to do with the content object
  
                # If this is another user's talk page, update newtalk.
                # Don't do this if $options['changed'] = false (null-edits) nor if
                }
  
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
 -                      MessageCache::singleton()->replace( $shortTitle, $text );
 +                      #XXX: could skip pseudo-messages like js/css here, based on content model.
 +                      $msgtext = $content->getWikitextForTransclusion();
 +                      if ( $msgtext === false || $msgtext === null ) $msgtext = '';
 +
 +                      MessageCache::singleton()->replace( $shortTitle, $msgtext );
                }
  
                if( $options['created'] ) {
         * @param $user User The relevant user
         * @param $comment String: comment submitted
         * @param $minor Boolean: whereas it's a minor modification
 +       *
 +       * @deprecated since 1.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 $status;
                }
  
 -              $this->doDeleteUpdates( $id );
 +              $this->doDeleteUpdates( $id, $content );
  
                # Log the deletion, if the page was suppressed, log it at Oversight instead
                $logtype = $suppress ? 'suppress' : 'delete';
         * 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
                $target = Revision::newFromId( $s->rev_id );
                if ( empty( $summary ) ) {
                        if ( $from == '' ) { // no public user name
-                               $summary = wfMessage( 'revertpage-nouser' )->inContentLanguage()->text();
+                               $summary = wfMessage( 'revertpage-nouser' );
                        } else {
-                               $summary = wfMessage( 'revertpage' )->inContentLanguage()->text();
+                               $summary = wfMessage( 'revertpage' );
                        }
                }
  
                        $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
                        $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
                );
-               $summary = wfMsgReplaceArgs( $summary, $args );
+               if( $summary instanceof Message ) {
+                       $summary = $summary->params( $args )->inContentLanguage()->text();
+               } else {
+                       $summary = wfMsgReplaceArgs( $summary, $args );
+               }
  
                # Truncate for whole multibyte characters.
                $summary = $wgContLang->truncate( $summary, 255 );
                }
  
                # Actually store the edit
 -              $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId(), $guser );
 +              $status = $this->doEditContent( $target->getContent(), $summary, $flags, $target->getId(), $guser );
 +
 +              if ( !$status->isOK() ) {
 +                      return $status->getErrorsArray();
 +              }
 +
                if ( !empty( $status->value['revision'] ) ) {
                        $revId = $status->value['revision']->getId();
                } else {
  
        /**
        * Return an applicable autosummary if one exists for the given edit.
 -      * @param $oldtext String: the previous text of the page.
 -      * @param $newtext String: The submitted text of the page.
 +      * @param $oldtext String|null: the previous text of the page.
 +      * @param $newtext String|null: The submitted text of the page.
        * @param $flags Int bitmask: a bitmask of flags submitted for the edit.
        * @return string An appropriate autosummary, or an empty string.
 +      *
 +      * @deprecated since 1.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, 255
 -                                      - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
 -                                      - strlen( $rt->getFullText() )
 -                              ) );
 -                      return wfMessage( 'autoredircomment', $rt->getFullText() )
 -                              ->rawParams( $truncatedtext )->inContentLanguage()->text();
 -              }
 -
 -              # New page autosummaries
 -              if ( $flags & EDIT_NEW && strlen( $newtext ) ) {
 -                      # If they're making a new article, give its text, truncated, in the summary.
 -
 -                      $truncatedtext = $wgContLang->truncate(
 -                              str_replace( "\n", ' ', $newtext ),
 -                              max( 0, 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) ) );
 -
 -                      return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
 -                              ->inContentLanguage()->text();
 -              }
 -
 -              # Blanking autosummaries
 -              if ( $oldtext != '' && $newtext == '' ) {
 -                      return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
 -              } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) {
 -                      # Removing more than 90% of the article
 +              # 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( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ) );
 +              wfDeprecated( __METHOD__, '1.WD' );
  
 -                      return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
 -                              ->inContentLanguage()->text();
 -              }
 +              $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
 +              $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
 +              $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
  
 -              # If we reach this point, there's no applicable autosummary for our case, so our
 -              # autosummary is empty.
 -              return '';
 +              return $handler->getAutosummary( $oldContent, $newContent, $flags );
        }
  
        /**
         *    if no revision occurred
         */
        public function getAutoDeleteReason( &$hasHistory ) {
 -              global $wgContLang;
 -
 -              // Get the last revision
 -              $rev = $this->getRevision();
 -
 -              if ( is_null( $rev ) ) {
 -                      return false;
 -              }
 -
 -              // Get the article's contents
 -              $contents = $rev->getText();
 -              $blank = false;
 -
 -              // If the page is blank, use the text from the previous revision,
 -              // which can only be blank if there's a move/import/protect dummy revision involved
 -              if ( $contents == '' ) {
 -                      $prev = $rev->getPrevious();
 -
 -                      if ( $prev )    {
 -                              $contents = $prev->getText();
 -                              $blank = true;
 -                      }
 -              }
 -
 -              $dbw = wfGetDB( DB_MASTER );
 -
 -              // Find out if there was only one contributor
 -              // Only scan the last 20 revisions
 -              $res = $dbw->select( 'revision', 'rev_user_text',
 -                      array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
 -                      __METHOD__,
 -                      array( 'LIMIT' => 20 )
 -              );
 -
 -              if ( $res === false ) {
 -                      // This page has no revisions, which is very weird
 -                      return false;
 -              }
 -
 -              $hasHistory = ( $res->numRows() > 1 );
 -              $row = $dbw->fetchObject( $res );
 -
 -              if ( $row ) { // $row is false if the only contributor is hidden
 -                      $onlyAuthor = $row->rev_user_text;
 -                      // Try to find a second contributor
 -                      foreach ( $res as $row ) {
 -                              if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
 -                                      $onlyAuthor = false;
 -                                      break;
 -                              }
 -                      }
 -              } else {
 -                      $onlyAuthor = false;
 -              }
 -
 -              // Generate the summary with a '$1' placeholder
 -              if ( $blank ) {
 -                      // The current revision is blank and the one before is also
 -                      // blank. It's just not our lucky day
 -                      $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
 -              } else {
 -                      if ( $onlyAuthor ) {
 -                              $reason = wfMessage(
 -                                      'excontentauthor',
 -                                      '$1',
 -                                      $onlyAuthor
 -                              )->inContentLanguage()->text();
 -                      } else {
 -                              $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
 -                      }
 -              }
 -
 -              if ( $reason == '-' ) {
 -                      // Allow these UI messages to be blanked out cleanly
 -                      return '';
 -              }
 -
 -              // Replace newlines with spaces to prevent uglyness
 -              $contents = preg_replace( "/[\n\r]/", ' ', $contents );
 -              // Calculate the maximum amount of chars to get
 -              // Max content length = max comment length - length of the comment (excl. $1)
 -              $maxLength = 255 - ( strlen( $reason ) - 2 );
 -              $contents = $wgContLang->truncate( $contents, $maxLength );
 -              // Remove possible unfinished links
 -              $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
 -              // Now replace the '$1' placeholder
 -              $reason = str_replace( '$1', $contents, $reason );
 -
 -              return $reason;
 +              return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
        }
  
        /**
                global $wgUser;
                return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid );
        }
-                       $updates = $content->getDeletionUpdates( $this->mTitle );
 +
 +      /**
 +       * Returns a list of updates to be performed when this page is deleted. The updates should remove any information
 +       * about this page from secondary data stores such as links tables.
 +       *
 +       * @param Content|null $content optional Content object for determining the necessary updates
 +       * @return Array an array of DataUpdates objects
 +       */
 +      public function getDeletionUpdates( Content $content = null ) {
 +              if ( !$content ) {
 +                      // load content object, which may be used to determine the necessary updates
 +                      // XXX: the content may not be needed to determine the updates, then this would be overhead.
 +                      $content = $this->getContent( Revision::RAW );
 +              }
 +
 +              if ( !$content ) {
 +                      $updates = array();
 +              } else {
++                      $updates = $content->getDeletionUpdates( $this );
 +              }
 +
 +              wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) );
 +              return $updates;
 +      }
 +
  }
  
  class PoolWorkArticleView extends PoolCounterWork {
        private $parserOptions;
  
        /**
 -       * @var string|null
 +       * @var Content|null
         */
 -      private $text;
 +      private $content = null;
  
        /**
         * @var ParserOutput|bool
         * @param $revid Integer: ID of the revision being parsed
         * @param $useParserCache Boolean: whether to use the parser cache
         * @param $parserOptions parserOptions to use for the parse operation
 -       * @param $text String: text to parse or null to load it
 +       * @param $content Content|String: content to parse or null to load it; may also be given as a wikitext string, for BC
         */
 -      function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) {
 +      function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null ) {
 +              if ( is_string($content) ) { #BC: old style call
 +                      $modelId = $page->getRevision()->getContentModel();
 +                      $format = $page->getRevision()->getContentFormat();
 +                      $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format );
 +              }
 +
                $this->page = $page;
                $this->revid = $revid;
                $this->cacheable = $useParserCache;
                $this->parserOptions = $parserOptions;
 -              $this->text = $text;
 +              $this->content = $content;
                $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions );
                parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid );
        }
         * @return bool
         */
        function doWork() {
 -              global $wgParser, $wgUseFileCache;
 +              global $wgUseFileCache;
 +
 +              // @todo: several of the methods called on $this->page are not declared in Page, but present
 +              //        in WikiPage and delegated by Article.
  
                $isCurrent = $this->revid === $this->page->getLatest();
  
 -              if ( $this->text !== null ) {
 -                      $text = $this->text;
 +              if ( $this->content !== null ) {
 +                      $content = $this->content;
                } elseif ( $isCurrent ) {
 -                      $text = $this->page->getRawText();
 +                      #XXX: why use RAW audience here, and PUBLIC (default) below?
 +                      $content = $this->page->getContent( Revision::RAW );
                } else {
                        $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid );
                        if ( $rev === null ) {
                                return false;
                        }
 -                      $text = $rev->getText();
 +
 +                      #XXX: why use PUBLIC audience here (default), and RAW above?
 +                      $content = $rev->getContent();
                }
  
                $time = - microtime( true );
 -              $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(),
 -                      $this->parserOptions, true, true, $this->revid );
 +              $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions );
                $time += microtime( true );
  
                # Timing hack
                return false;
        }
  }
 +
diff --combined includes/api/ApiMain.php
@@@ -105,7 -105,6 +105,7 @@@ class ApiMain extends ApiBase 
                'dbgfm' => 'ApiFormatDbg',
                'dump' => 'ApiFormatDump',
                'dumpfm' => 'ApiFormatDump',
 +              'none' => 'ApiFormatNone',
        );
  
        /**
                                $this->dieReadOnly();
                        }
                }
+               // Allow extensions to stop execution for arbitrary reasons.
+               $message = false;
+               if( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) {
+                       $this->dieUsageMsg( $message );
+               }
        }
  
        /**
@@@ -1154,7 -1154,9 +1154,9 @@@ class LocalFile extends File 
                $logId = $log->addEntry( $action, $descTitle, $comment, array(), $user );
  
                wfProfileIn( __METHOD__ . '-edit' );
-               if ( $descTitle->exists() ) {
+               $exists = $descTitle->exists();
+               if ( $exists ) {
                        # Create a null revision
                        $latest = $descTitle->getLatestRevID();
                        $nullRevision = Revision::newNullRevision(
                                wfRunHooks( 'NewRevisionFromEditComplete', array( $wikiPage, $nullRevision, $latest, $user ) );
                                $wikiPage->updateRevisionOn( $dbw, $nullRevision );
                        }
-                       $dbw->update( 'logging', array( 'log_page' => $descTitle->getArticleID() ), array( 'log_id' => $logId ), __METHOD__ );
+               }
+               # Commit the transaction now, in case something goes wrong later
+               # The most important thing is that files don't get lost, especially archives
+               # NOTE: once we have support for nested transactions, the commit may be moved
+               #       to after $wikiPage->doEdit has been called.
+               $dbw->commit( __METHOD__ );
  
+               if ( $exists ) {
                        # Invalidate the cache for the description page
                        $descTitle->invalidateCache();
                        $descTitle->purgeSquid();
                } else {
                        # New file; create the description page.
                        # There's already a log entry, so don't make a second RC entry
 -                      # Squid and file cache for the description page are purged by doEdit.
 -                      $status = $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
 +                      # Squid and file cache for the description page are purged by doEditContent.
 +                      $content = ContentHandler::makeContent( $pageText, $descTitle );
 +                      $status = $wikiPage->doEditContent( $content, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
  
-                       if ( isset( $status->value['revision'] ) ) {
-                               $dbw->update( 'logging', array( 'log_page' => $status->value['revision']->getPage() ), array( 'log_id' => $logId ), __METHOD__ );
+                       if ( isset( $status->value['revision'] ) ) { // XXX; doEdit() uses a transaction
+                               $dbw->begin();
+                               $dbw->update( 'logging',
+                                       array( 'log_page' => $status->value['revision']->getPage() ),
+                                       array( 'log_id' => $logId ),
+                                       __METHOD__
+                               );
+                               $dbw->commit(); // commit before anything bad can happen
                        }
                }
                wfProfileOut( __METHOD__ . '-edit' );
  
-               # Commit the transaction now, in case something goes wrong later
-               # The most important thing is that files don't get lost, especially archives
-               $dbw->commit( __METHOD__ );
                # Save to cache and purge the squid
                # We shall not saveToCache before the commit since otherwise
                # in case of a rollback there is an usable file from memcached
                global $wgParser;
                $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
                if ( !$revision ) return false;
 -              $text = $revision->getText();
 -              if ( !$text ) return false;
 -              $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
 +              $content = $revision->getContent();
 +              if ( !$content ) return false;
 +              $pout = $content->getParserOutput( $this->title, null, new ParserOptions() );
                return $pout->getText();
        }
  
@@@ -206,7 -206,6 +206,7 @@@ class MysqlUpdater extends DatabaseUpda
                        array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ),
                        array( 'addField',      'uploadstash',  'us_chunk_inx',         'patch-uploadstash_chunk.sql' ),
                        array( 'addfield', 'job',           'job_timestamp',    'patch-jobs-add-timestamp.sql' ),
 +
                        array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ufg_group-length-increase.sql' ),
  
                        // 1.20
                        array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ),
                        array( 'addField', 'ipblocks',      'ipb_parent_block_id',           'patch-ipb-parent-block-id.sql' ),
                        array( 'addIndex', 'ipblocks',      'ipb_parent_block_id',           'patch-ipb-parent-block-id-index.sql' ),
+                       array( 'dropField', 'category',     'cat_hidden',       'patch-cat_hidden.sql' ),
 +
 +                      // 1.WD
 +                      array( 'addField',      'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
 +                      array( 'addField',      'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
 +                      array( 'addField',      'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
                );
        }
  
@@@ -85,7 -85,6 +85,7 @@@ class SqliteUpdater extends DatabaseUpd
                        array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ),
                        array( 'addField',      'uploadstash',  'us_chunk_inx',         'patch-uploadstash_chunk.sql' ),
                        array( 'addfield', 'job',           'job_timestamp',    'patch-jobs-add-timestamp.sql' ),
 +
                        array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ug_group-length-increase.sql' ),
  
                        // 1.20
                        array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ),
                        array( 'addField', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id.sql' ),
                        array( 'addIndex', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id-index.sql' ),
+                       array( 'dropField', 'category',     'cat_hidden',       'patch-cat_hidden.sql' ),
 +
 +                      // 1.WD
 +                      array( 'addField',      'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
 +                      array( 'addField',      'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
 +                      array( 'addField',      'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
                );
        }
  
@@@ -756,8 -756,10 +756,10 @@@ class SearchResult 
        protected function initFromTitle( $title ) {
                $this->mTitle = $title;
                if ( !is_null( $this->mTitle ) ) {
+                       $id = false;
+                       wfRunHooks( 'SearchResultInitFromTitle', array( $title, &$id ) );
                        $this->mRevision = Revision::newFromTitle(
-                               $this->mTitle, false, Revision::READ_NORMAL );
+                               $this->mTitle, $id, Revision::READ_NORMAL );
                        if ( $this->mTitle->getNamespace() === NS_FILE )
                                $this->mImage = wfFindFile( $this->mTitle );
                }
         */
        protected function initText() {
                if ( !isset( $this->mText ) ) {
 -                      if ( $this->mRevision != null )
 -                              $this->mText = $this->mRevision->getText();
 -                      else // TODO: can we fetch raw wikitext for commons images?
 +                      if ( $this->mRevision != null ) {
 +                              //TODO: don't use the text, but the content object!
 +                              $content = $this->mRevision->getContent();
 +                              $this->mText = $content->getTextForSearchIndex();
 +                      } else { // TODO: can we fetch raw wikitext for commons images?
                                $this->mText = '';
 -
 +                      }
                }
        }
  
        function getTextSnippet( $terms ) {
                global $wgUser, $wgAdvancedSearchHighlighting;
                $this->initText();
 +
 +              // TODO: make highliter take a content object. Make ContentHandler a factory for SearchHighliter.
                list( $contextlines, $contextchars ) = SearchEngine::userHighlightPrefs( $wgUser );
                $h = new SearchHighlighter();
                if ( $wgAdvancedSearchHighlighting )
@@@ -931,7 -931,7 +931,7 @@@ Möglicherweise hast du dein Passwort b
  'passwordreset-text' => 'Bitte dieses Formular ausfüllen, um per E-Mail eine Erinnerung zu den Anmeldeinformationen deines Benutzerkontos zu erhalten.',
  'passwordreset-legend' => 'Passwort zurücksetzen',
  'passwordreset-disabled' => 'Das Zurücksetzen von Passwörtern wurde in diesem Wiki deaktiviert.',
- 'passwordreset-pretext' => '{{PLURAL:$1||Gib eines der folgenden Daten ein}}',
+ 'passwordreset-pretext' => '{{PLURAL:$1||Gib eines der folgenden Daten ein.}}',
  'passwordreset-username' => 'Benutzername:',
  'passwordreset-domain' => 'Domain:',
  'passwordreset-capture' => 'Die E-Mail-Nachricht ansehen?',
@@@ -1317,7 -1317,9 +1317,9 @@@ Bitte prüfe die Logbücher.'
  'revdelete-only-restricted' => 'Fehler beim Verstecken des Eintrags vom $1, $2 Uhr: Du kannst keinen Eintrag vor Administratoren verstecken, ohne eine der anderen Ansichtsoptionen gewählt zu haben.',
  'revdelete-reason-dropdown' => '*Allgemeine Löschgründe
  ** Urheberrechtsverletzung
- ** Unangebrachte persönliche Informationen',
+ ** Unangebrachte Kommentare oder persönliche Informationen
+ ** Unangebrachter Benutzername
+ ** Potentiell beleidigende Informationen',
  'revdelete-otherreason' => 'Anderer/ergänzender Grund:',
  'revdelete-reasonotherlist' => 'Anderer Grund',
  'revdelete-edit-reasonlist' => 'Löschgründe bearbeiten',
@@@ -1678,8 -1680,8 +1680,8 @@@ Dies kann nicht mehr rückgängig gemac
  # User rights log
  'rightslog' => 'Rechte-Logbuch',
  'rightslogtext' => 'Dies ist das Logbuch der Änderungen der Benutzerrechte.',
- 'rightslogentry' => 'änderte die Benutzerrechte für „$1“ von „$2“ auf „$3“',
- 'rightslogentry-autopromote' => 'wurde automatisch von „$2“ nach „$3“ zugeordnet',
+ 'rightslogentry' => 'änderte die Benutzerrechte für „$1“ von „$2“ zu „$3“',
+ 'rightslogentry-autopromote' => 'wurde automatisch von „$2“ zu „$3“ zugeordnet',
  'rightsnone' => '(–)',
  
  # Associated actions - in the sentence "You do not have permission to X"
@@@ -2600,8 -2602,7 +2602,8 @@@ Der aktuelle Text der gelöschten Seit
  'undeletedrevisions' => '{{PLURAL:$1|1 Version wurde|$1 Versionen wurden}} wiederhergestellt',
  'undeletedrevisions-files' => '{{PLURAL:$1|1 Version|$1 Versionen}} und {{PLURAL:$2|1 Datei|$2 Dateien}} wurden wiederhergestellt',
  'undeletedfiles' => '{{PLURAL:$1|1 Datei wurde|$1 Dateien wurden}} wiederhergestellt',
 -'cannotundelete' => 'Wiederherstellung fehlgeschlagen; jemand anderes hat die Seite bereits wiederhergestellt.',
 +'cannotundelete' => 'Wiederherstellung fehlgeschlagen:
 +$1',
  'undeletedpage' => "'''„$1“''' wurde wiederhergestellt.
  
  Im [[Special:Log/delete|Lösch-Logbuch]] findest du eine Übersicht der gelöschten und wiederhergestellten Seiten.",
@@@ -2976,7 -2977,7 +2978,7 @@@ Alle Transwiki-Import-Aktionen werden i
  'import-interwiki-source' => 'Quell-Wiki/-Seite:',
  'import-interwiki-history' => 'Alle Versionen dieser Seite importieren',
  'import-interwiki-templates' => 'Alle Vorlagen einschließen',
- 'import-interwiki-submit' => 'Import',
+ 'import-interwiki-submit' => 'Importieren',
  'import-interwiki-namespace' => 'Zielnamensraum:',
  'import-interwiki-rootpage' => 'Zielstammseite (optional):',
  'import-upload-filename' => 'Dateiname:',
@@@ -3190,7 -3191,7 +3192,7 @@@ Das liegt wahrscheinlich an einem Link 
  'pageinfo-authors' => 'Gesamtzahl unterschiedlicher Autoren',
  'pageinfo-recent-edits' => 'Anzahl der kürzlich erfolgten Bearbeitungen (innerhalb von $1)',
  'pageinfo-recent-authors' => 'Anzahl der unterschiedlichen Autoren',
- 'pageinfo-restriction' => 'Seitenschutz (<code>{{lcfirst:$1}}</code>)',
+ 'pageinfo-restriction' => 'Seitenschutz ({{lcfirst:$1}})',
  'pageinfo-magic-words' => '{{PLURAL:$1|Magisches Wort|Magische Wörter}} ($1)',
  'pageinfo-hidden-categories' => 'Versteckte {{PLURAL:$1|Kategorie|Kategorien}} ($1)',
  'pageinfo-templates' => 'Eingebundene {{PLURAL:$1|Vorlage|Vorlagen}} ($1)',
@@@ -895,7 -895,6 +895,7 @@@ $1'
  'portal-url'           => 'Project:Community portal',
  'privacy'              => 'Privacy policy',
  'privacypage'          => 'Project:Privacy policy',
 +'content-failed-to-parse' => "Failed to parse $2 content for $1 model: $3",
  
  'badaccess'        => 'Permission error',
  'badaccess-group0' => 'You are not allowed to execute the action you have requested.',
@@@ -1430,7 -1429,7 +1430,7 @@@ If you save it, any changes made since 
  'yourdiff'                         => 'Differences',
  'copyrightwarning'                 => "Please note that all contributions to {{SITENAME}} are considered to be released under the $2 (see $1 for details).
  If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.<br />
 -You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource.
 +You are also promising us that you wrote this yourself, or copied editpageit from a public domain or similar free resource.
  '''Do not submit copyrighted work without permission!'''",
  'copyrightwarning2'                => "Please note that all contributions to {{SITENAME}} may be edited, altered, or removed by other contributors.
  If you do not want your writing to be edited mercilessly, then do not submit it here.<br />
@@@ -1487,7 -1486,6 +1487,7 @@@ It already exists.'
  'addsection-preload'               => '', # do not translate or duplicate this message to other languages
  'addsection-editintro'             => '', # do not translate or duplicate this message to other languages
  'defaultmessagetext'               => 'Default message text',
 +'invalid-content-data'             => 'Invalid content data',
  
  # Parser/template warnings
  'expensive-parserfunction-warning'        => "'''Warning:''' This page contains too many expensive parser function calls.
@@@ -1649,7 -1647,8 +1649,8 @@@ Please check the logs.'
  'revdelete-only-restricted'   => 'Error hiding the item dated $2, $1: You cannot suppress items from view by administrators without also selecting one of the other visibility options.',
  'revdelete-reason-dropdown'   => '*Common delete reasons
  ** Copyright violation
- ** Inappropriate personal information
+ ** Inappropriate comment or personal information
+ ** Inappropriate username
  ** Potentially libelous information',
  'revdelete-otherreason'       => 'Other/additional reason:',
  'revdelete-reasonotherlist'   => 'Other reason',
@@@ -3070,8 -3069,8 +3071,8 @@@ You may have a bad link, or the revisio
  'undeletedrevisions'           => '{{PLURAL:$1|1 revision|$1 revisions}} restored',
  'undeletedrevisions-files'     => '{{PLURAL:$1|1 revision|$1 revisions}} and {{PLURAL:$2|1 file|$2 files}} restored',
  'undeletedfiles'               => '{{PLURAL:$1|1 file|$1 files}} restored',
 -'cannotundelete'               => 'Undelete failed;
 -someone else may have undeleted the page first.',
 +'cannotundelete'               => 'Undelete failed:
 +$1',
  'undeletedpage'                => "'''$1 has been restored'''
  
  Consult the [[Special:Log/delete|deletion log]] for a record of recent deletions and restorations.",
@@@ -3764,7 -3763,7 +3765,7 @@@ This is probably caused by a link to a 
  'pageinfo-authors'             => 'Total number of distinct authors',
  'pageinfo-recent-edits'        => 'Recent number of edits (within past $1)',
  'pageinfo-recent-authors'      => 'Recent number of distinct authors',
- 'pageinfo-restriction'         => 'Page protection (<code>{{lcfirst:$1}}</code>)',
+ 'pageinfo-restriction'         => 'Page protection ({{lcfirst:$1}})',
  'pageinfo-magic-words'         => 'Magic {{PLURAL:$1|word|words}} ($1)',
  'pageinfo-hidden-categories'   => 'Hidden {{PLURAL:$1|category|categories}} ($1)',
  'pageinfo-templates'           => 'Transcluded {{PLURAL:$1|template|templates}} ($1)',
@@@ -4944,10 -4943,4 +4945,10 @@@ Otherwise, you can use the easy form be
  'duration-centuries' => '$1 {{PLURAL:$1|century|centuries}}',
  'duration-millennia' => '$1 {{PLURAL:$1|millennium|millennia}}',
  
 +# Content model IDs for the ContentHandler facility; used by ContentHandler::getContentModel()
 +'content-model-wikitext' => 'wikitext',
 +'content-model-javascript' => 'JavaScript',
 +'content-model-css' => 'CSS',
 +'content-model-text' => 'plain text',
 +
  );
@@@ -26,6 -26,7 +26,7 @@@
   * @author Brest
   * @author BrokenArrow
   * @author Byrial
+  * @author BáthoryPéter
   * @author Claudia Hattitten
   * @author Codex Sinaiticus
   * @author Crt
@@@ -462,7 -463,8 +463,8 @@@ Also used as title of [[Special:Search]
  'toolbox' => 'The title of the toolbox below the search menu.',
  'otherlanguages' => 'This message is shown under the toolbox. It is used if there are interwiki links added to the page, like <tt><nowiki>[[</nowiki>en:Interwiki article]]</tt>.
  {{Identical|Otherlanguages}}',
- 'redirectedfrom' => 'The text displayed when a certain page is redirected to another page. Variable <tt>$1</tt> contains the name of the page user came from.',
+ 'redirectedfrom' => 'The text displayed when a certain page is redirected to another page.
+ *<tt>$1</tt> contains the name of the page user came from.',
  'redirectpagesub' => 'Displayed under the page title of a page which is a redirect to another page, see [{{fullurl:Project:Translators|redirect=no}} Project:Translators] for example.
  
  {{Identical|Redirect page}}',
@@@ -544,7 -546,7 +546,7 @@@ Do '''not''' replace SITENAME with a tr
  Appears in subtitle
  * $1 is a link to the page (HTML)',
  'retrievedfrom' => 'Message which appears in the source of every page, but it is hidden. It is shown when printing. $1 is a link back to the current page: {{FULLURL:{{FULLPAGENAME}}}}.',
- 'youhavenewmessages' => 'The blue message appearing when someone edited your user talk page.
+ 'youhavenewmessages' => 'The yellow message appearing when someone edited your user talk page.
  The format is: "{{int:youhavenewmessages| [[MediaWiki:Newmessageslink/{{SUBPAGENAME}}|{{int:newmessageslink}}]] |[[MediaWiki:Newmessagesdifflink/{{SUBPAGENAME}}|{{int:newmessagesdifflink}}]]}}"',
  'newmessageslink' => 'This is the first link displayed in an orange rectangle when a user gets a message on his talk page. Used in message {{msg-mw|youhavenewmessages}} (as parameter $1).
  
@@@ -1051,7 -1053,6 +1053,7 @@@ Please report at [[Support]] if you ar
  'moveddeleted-notice' => 'Shown on top of a deleted page in normal view modus ([http://translatewiki.net/wiki/Test example]).',
  'edit-conflict' => "An 'Edit conflict' happens when more than one edit is being made to a page at the same time. This would usually be caused by separate individuals working on the same page. However, if the system is slow, several edits from one individual could back up and attempt to apply simultaneously - causing the conflict.",
  'defaultmessagetext' => 'Caption above the default message text shown on the left-hand side of a diff displayed after clicking “Show changes” when creating a new page in the MediaWiki: namespace',
 +'invalid-content-data'             => 'Error message indicating that the page\'s content can not be saved because it is invalid. This may occurr for some non-text content types.',
  
  # Parser/template warnings
  'expensive-parserfunction-warning' => 'On some (expensive) [[MetaWikipedia:Help:ParserFunctions|parser functions]] (e.g. <code><nowiki>{{#ifexist:}}</nowiki></code>) there is a limit of how many times it may be used. This is an error message shown when the limit is exceeded.
@@@ -2878,8 -2879,6 +2880,8 @@@ Options for the duration of the page pr
  {{Identical|Reset}}',
  'undeleteinvert' => '{{Identical|Invert selection}}',
  'undeletecomment' => '{{Identical|Reason}}',
 +'cannotundelete' => 'Message shown when undeletion failed for some reason.
 +* <code>$1</code> is the combined wikitext of messages for all errors that caused the failure.',
  'undelete-search-title' => 'Page title when showing the search form in Special:Undelete',
  'undelete-search-submit' => '{{Identical|Search}}',
  'undelete-error' => 'Page title when a page could not be undeleted',
@@@ -3492,7 -3491,7 +3494,7 @@@ See also {{msg-mw|Anonuser}} and {{msg-
  'pageinfo-recent-edits' => 'The number of times the page has been edited recently. $1 is a localised duration (e.g. 9 days).',
  'pageinfo-recent-authors' => 'The number of users who have edited the page recently.',
  'pageinfo-restriction' => 'Parameters:
- * $1 is the type of page protection (message restriction-$type, preferably in lowercase). If your language doesn\'t have small and capital letters, you can simply write <nowiki><code>$1</code></nowiki>.',
+ * $1 is the type of page protection (message restriction-$type, preferably in lowercase). If your language doesn\'t have small and capital letters, you can simply write <nowiki>$1</nowiki>.',
  'pageinfo-magic-words' => 'The list of magic words on the page. Parameters:
  * $1 is the number of magic words on the page.',
  'pageinfo-hidden-categories' => 'The list of hidden categories on the page. Parameters:
@@@ -4878,10 -4877,4 +4880,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.',
 +
  );
diff --combined maintenance/tables.sql
@@@ -260,10 -260,7 +260,10 @@@ CREATE TABLE /*_*/page 
    page_latest int unsigned NOT NULL,
  
    -- Uncompressed length in bytes of the page's current source text.
 -  page_len int unsigned NOT NULL
 +  page_len int unsigned NOT NULL,
 +
 +  -- content model, see CONTENT_MODEL_XXX constants
 +  page_content_model  int unsigned  default NULL
  ) /*$wgDBTableOptions*/;
  
  CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title);
@@@ -319,13 -316,7 +319,13 @@@ CREATE TABLE /*_*/revision 
    rev_parent_id int unsigned default NULL,
  
    -- SHA-1 text content hash in base-36
 -  rev_sha1 varbinary(32) NOT NULL default ''
 +  rev_sha1 varbinary(32) NOT NULL default '',
 +
 +  -- content model, see CONTENT_MODEL_XXX constants
 +  rev_content_model  int unsigned  default NULL,
 +
 +  -- content format, see CONTENT_FORMAT_XXX constants
 +  rev_content_format int unsigned  default NULL
  
  ) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024;
  -- In case tables are created as MyISAM, use row hints for MySQL <5.0 to avoid 4GB limit
@@@ -436,14 -427,7 +436,14 @@@ CREATE TABLE /*_*/archive 
    ar_parent_id int unsigned default NULL,
  
    -- SHA-1 text content hash in base-36
 -  ar_sha1 varbinary(32) NOT NULL default ''
 +  ar_sha1 varbinary(32) NOT NULL default '',
 +
 +  -- content model, see CONTENT_MODEL_XXX constants
 +  ar_content_model  int unsigned default NULL,
 +
 +  -- content format, see CONTENT_FORMAT_XXX constants
 +  ar_content_format int unsigned default NULL
 +
  ) /*$wgDBTableOptions*/;
  
  CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp);
@@@ -587,10 -571,7 +587,7 @@@ CREATE TABLE /*_*/category 
    -- ing is not.
    cat_pages int signed NOT NULL default 0,
    cat_subcats int signed NOT NULL default 0,
-   cat_files int signed NOT NULL default 0,
-   -- Reserved for future use
-   cat_hidden tinyint unsigned NOT NULL default 0
+   cat_files int signed NOT NULL default 0
  ) /*$wgDBTableOptions*/;
  
  CREATE UNIQUE INDEX /*i*/cat_title ON /*_*/category (cat_title);
index 942e152,0000000..7a7ba91
mode 100644,000000..100644
--- /dev/null
@@@ -1,552 -1,0 +1,552 @@@
-               $updates = $content->getDeletionUpdates( $title );
 +<?php
 +
 +/**
 + * @group ContentHandler
 + *
 + * @group Database
 + *        ^--- needed, because we do need the database to test link updates
 + */
 +class WikitextContentTest extends MediaWikiTestCase {
 +
 +      public function setup() {
 +              global $wgUser;
 +
 +              // anon user
 +              $wgUser = new User();
 +              $wgUser->setName( '127.0.0.1' );
 +
 +              $this->context = new RequestContext( new FauxRequest() );
 +              $this->context->setTitle( Title::newFromText( "Test" ) );
 +              $this->context->setUser( $wgUser );
 +      }
 +
 +      public function newContent( $text ) {
 +              return new WikitextContent( $text );
 +      }
 +
 +
 +      public function dataGetParserOutput() {
 +              return array(
 +                      array("WikitextContentTest_testGetParserOutput", "hello ''world''\n", "<p>hello <i>world</i>\n</p>"),
 +                      // @todo: more...?
 +              );
 +      }
 +
 +      /**
 +       * @dataProvider dataGetParserOutput
 +       */
 +      public function testGetParserOutput( $title, $text, $expectedHtml ) {
 +              $title = Title::newFromText( $title );
 +              $content = ContentHandler::makeContent( $text, $title );
 +
 +              $po = $content->getParserOutput( $title );
 +
 +              $this->assertEquals( $expectedHtml, $po->getText() );
 +              // @todo: assert more properties
 +      }
 +
 +      public function dataGetSecondaryDataUpdates() {
 +              return array(
 +                      array("WikitextContentTest_testGetSecondaryDataUpdates_1", "hello ''world''\n",
 +                              array( 'LinksUpdate' => array(  'mRecursive' => true,
 +                                                              'mLinks' => array() ) )
 +                      ),
 +                      array("WikitextContentTest_testGetSecondaryDataUpdates_2", "hello [[world test 21344]]\n",
 +                              array( 'LinksUpdate' => array(  'mRecursive' => true,
 +                                                              'mLinks' => array( array( 'World_test_21344' => 0 ) ) ) )
 +                      ),
 +                      // @todo: more...?
 +              );
 +      }
 +
 +      /**
 +       * @dataProvider dataGetSecondaryDataUpdates
 +       * @group Database
 +       */
 +      public function testGetSecondaryDataUpdates( $title, $text, $expectedStuff ) {
 +              $title = Title::newFromText( $title );
 +              $title->resetArticleID( 2342 ); //dummy id. fine as long as we don't try to execute the updates!
 +
 +              $handler = ContentHandler::getForModelID( $title->getContentModel() );
 +              $content = ContentHandler::makeContent( $text, $title );
 +
 +              $updates = $content->getSecondaryDataUpdates( $title );
 +
 +              // make updates accessible by class name
 +              foreach ( $updates as $update ) {
 +                      $class = get_class( $update );
 +                      $updates[ $class ] = $update;
 +              }
 +
 +              foreach ( $expectedStuff as $class => $fieldValues ) {
 +                      $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" );
 +
 +                      $update = $updates[ $class ];
 +
 +                      foreach ( $fieldValues as $field => $value ) {
 +                              $v = $update->$field; #if the field doesn't exist, just crash and burn
 +                              $this->assertEquals( $value, $v, "unexpected value for field $field in instance of $class" );
 +                      }
 +              }
 +      }
 +
 +
 +      static $sections =
 +
 +"Intro
 +
 +== stuff ==
 +hello world
 +
 +== test ==
 +just a test
 +
 +== foo ==
 +more stuff
 +";
 +
 +      public function dataGetSection() {
 +              return array(
 +                      array( WikitextContentTest::$sections,
 +                                      "0",
 +                                      "Intro"
 +                      ),
 +                      array( WikitextContentTest::$sections,
 +                                      "2",
 +"== test ==
 +just a test"
 +                      ),
 +                      array( WikitextContentTest::$sections,
 +                                      "8",
 +                                      false
 +                      ),
 +              );
 +      }
 +
 +      /**
 +       * @dataProvider dataGetSection
 +       */
 +      public function testGetSection( $text, $sectionId, $expectedText ) {
 +              $content = $this->newContent( $text );
 +
 +              $sectionContent = $content->getSection( $sectionId );
 +
 +              $this->assertEquals( $expectedText, is_null( $sectionContent ) ? null : $sectionContent->getNativeData() );
 +      }
 +
 +      public function dataReplaceSection() {
 +              return array(
 +                      array( WikitextContentTest::$sections,
 +                             "0",
 +                             "No more",
 +                             null,
 +                             trim( preg_replace( '/^Intro/sm', 'No more', WikitextContentTest::$sections ) )
 +                      ),
 +                      array( WikitextContentTest::$sections,
 +                             "",
 +                             "No more",
 +                             null,
 +                             "No more"
 +                      ),
 +                      array( WikitextContentTest::$sections,
 +                             "2",
 +                             "== TEST ==\nmore fun",
 +                             null,
 +                             trim( preg_replace( '/^== test ==.*== foo ==/sm', "== TEST ==\nmore fun\n\n== foo ==", WikitextContentTest::$sections ) )
 +                      ),
 +                      array( WikitextContentTest::$sections,
 +                             "8",
 +                             "No more",
 +                             null,
 +                             WikitextContentTest::$sections
 +                      ),
 +                      array( WikitextContentTest::$sections,
 +                             "new",
 +                             "No more",
 +                             "New",
 +                             trim( WikitextContentTest::$sections ) . "\n\n\n== New ==\n\nNo more"
 +                      ),
 +              );
 +      }
 +
 +      /**
 +       * @dataProvider dataReplaceSection
 +       */
 +      public function testReplaceSection( $text, $section, $with, $sectionTitle, $expected ) {
 +              $content = $this->newContent( $text );
 +              $c = $content->replaceSection( $section, $this->newContent( $with ), $sectionTitle );
 +
 +              $this->assertEquals( $expected, is_null( $c ) ? null : $c->getNativeData() );
 +      }
 +
 +      public function testAddSectionHeader( ) {
 +              $content = $this->newContent( 'hello world' );
 +              $content = $content->addSectionHeader( 'test' );
 +
 +              $this->assertEquals( "== test ==\n\nhello world", $content->getNativeData() );
 +      }
 +
 +      public function dataPreSaveTransform() {
 +              return array(
 +                      array( 'hello this is ~~~',
 +                             "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
 +                      ),
 +                      array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
 +                             'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
 +                      ),
 +              );
 +      }
 +
 +      /**
 +       * @dataProvider dataPreSaveTransform
 +       */
 +      public function testPreSaveTransform( $text, $expected ) {
 +              global $wgContLang;
 +
 +              $options = ParserOptions::newFromUserAndLang( $this->context->getUser(), $wgContLang );
 +
 +              $content = $this->newContent( $text );
 +              $content = $content->preSaveTransform( $this->context->getTitle(), $this->context->getUser(), $options );
 +
 +              $this->assertEquals( $expected, $content->getNativeData() );
 +      }
 +
 +      public function dataPreloadTransform() {
 +              return array(
 +                      array( 'hello this is ~~~',
 +                             "hello this is ~~~",
 +                      ),
 +                      array( 'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>',
 +                             'hello \'\'this\'\' is bar',
 +                      ),
 +              );
 +      }
 +
 +      /**
 +       * @dataProvider dataPreloadTransform
 +       */
 +      public function testPreloadTransform( $text, $expected ) {
 +              global $wgContLang;
 +              $options = ParserOptions::newFromUserAndLang( $this->context->getUser(), $wgContLang );
 +
 +              $content = $this->newContent( $text );
 +              $content = $content->preloadTransform( $this->context->getTitle(), $options );
 +
 +              $this->assertEquals( $expected, $content->getNativeData() );
 +      }
 +
 +      public function dataGetRedirectTarget() {
 +              return array(
 +                      array( '#REDIRECT [[Test]]',
 +                             'Test',
 +                      ),
 +                      array( '#REDIRECT Test',
 +                             null,
 +                      ),
 +                      array( '* #REDIRECT [[Test]]',
 +                             null,
 +                      ),
 +              );
 +      }
 +
 +      /**
 +       * @dataProvider dataGetRedirectTarget
 +       */
 +      public function testGetRedirectTarget( $text, $expected ) {
 +              $content = $this->newContent( $text );
 +              $t = $content->getRedirectTarget( );
 +
 +              if ( is_null( $expected ) ) $this->assertNull( $t, "text should not have generated a redirect target: $text" );
 +              else $this->assertEquals( $expected, $t->getPrefixedText() );
 +      }
 +
 +      /**
 +       * @dataProvider dataGetRedirectTarget
 +       */
 +      public function isRedirect( $text, $expected ) {
 +              $content = $this->newContent( $text );
 +
 +              $this->assertEquals( !is_null($expected), $content->isRedirect() );
 +      }
 +
 +
 +      /**
 +       * @todo: test needs database!
 +       */
 +      /*
 +      public function getRedirectChain() {
 +              $text = $this->getNativeData();
 +              return Title::newFromRedirectArray( $text );
 +      }
 +      */
 +
 +      /**
 +       * @todo: test needs database!
 +       */
 +      /*
 +      public function getUltimateRedirectTarget() {
 +              $text = $this->getNativeData();
 +              return Title::newFromRedirectRecurse( $text );
 +      }
 +      */
 +
 +
 +      public function dataIsCountable() {
 +              return array(
 +                      array( '',
 +                             null,
 +                             'any',
 +                             true
 +                      ),
 +                      array( 'Foo',
 +                             null,
 +                             'any',
 +                             true
 +                      ),
 +                      array( 'Foo',
 +                             null,
 +                             'comma',
 +                             false
 +                      ),
 +                      array( 'Foo, bar',
 +                             null,
 +                             'comma',
 +                             true
 +                      ),
 +                      array( 'Foo',
 +                             null,
 +                             'link',
 +                             false
 +                      ),
 +                      array( 'Foo [[bar]]',
 +                             null,
 +                             'link',
 +                             true
 +                      ),
 +                      array( 'Foo',
 +                             true,
 +                             'link',
 +                             true
 +                      ),
 +                      array( 'Foo [[bar]]',
 +                             false,
 +                             'link',
 +                             false
 +                      ),
 +                      array( '#REDIRECT [[bar]]',
 +                             true,
 +                             'any',
 +                             false
 +                      ),
 +                      array( '#REDIRECT [[bar]]',
 +                             true,
 +                             'comma',
 +                             false
 +                      ),
 +                      array( '#REDIRECT [[bar]]',
 +                             true,
 +                             'link',
 +                             false
 +                      ),
 +              );
 +      }
 +
 +
 +      /**
 +       * @dataProvider dataIsCountable
 +       * @group Database
 +       */
 +      public function testIsCountable( $text, $hasLinks, $mode, $expected ) {
 +              global $wgArticleCountMethod;
 +
 +              $old = $wgArticleCountMethod;
 +              $wgArticleCountMethod = $mode;
 +
 +              $content = $this->newContent( $text );
 +
 +              $v = $content->isCountable( $hasLinks, $this->context->getTitle() );
 +              $wgArticleCountMethod = $old;
 +
 +              $this->assertEquals( $expected, $v, "isCountable() returned unexpected value " . var_export( $v, true )
 +                                                  . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" );
 +      }
 +
 +      public function dataGetTextForSummary() {
 +              return array(
 +                      array( "hello\nworld.",
 +                             16,
 +                             'hello world.',
 +                      ),
 +                      array( 'hello world.',
 +                             8,
 +                             'hello...',
 +                      ),
 +                      array( '[[hello world]].',
 +                             8,
 +                             'hel...',
 +                      ),
 +              );
 +      }
 +
 +      /**
 +       * @dataProvider dataGetTextForSummary
 +       */
 +      public function testGetTextForSummary( $text, $maxlength, $expected ) {
 +              $content = $this->newContent( $text );
 +
 +              $this->assertEquals( $expected, $content->getTextForSummary( $maxlength ) );
 +      }
 +
 +
 +      public function testGetTextForSearchIndex( ) {
 +              $content = $this->newContent( "hello world." );
 +
 +              $this->assertEquals( "hello world.", $content->getTextForSearchIndex() );
 +      }
 +
 +      public function testCopy() {
 +              $content = $this->newContent( "hello world." );
 +              $copy = $content->copy();
 +
 +              $this->assertTrue( $content->equals( $copy ), "copy must be equal to original" );
 +              $this->assertEquals( "hello world.", $copy->getNativeData() );
 +      }
 +
 +      public function testGetSize( ) {
 +              $content = $this->newContent( "hello world." );
 +
 +              $this->assertEquals( 12, $content->getSize() );
 +      }
 +
 +      public function testGetNativeData( ) {
 +              $content = $this->newContent( "hello world." );
 +
 +              $this->assertEquals( "hello world.", $content->getNativeData() );
 +      }
 +
 +      public function testGetWikitextForTransclusion( ) {
 +              $content = $this->newContent( "hello world." );
 +
 +              $this->assertEquals( "hello world.", $content->getWikitextForTransclusion() );
 +      }
 +
 +      public function testMatchMagicWord( ) {
 +              $mw = MagicWord::get( "staticredirect" );
 +
 +              $content = $this->newContent( "#REDIRECT [[FOO]]\n__STATICREDIRECT__" );
 +              $this->assertTrue( $content->matchMagicWord( $mw ), "should have matched magic word" );
 +
 +              $content = $this->newContent( "#REDIRECT [[FOO]]" );
 +              $this->assertFalse( $content->matchMagicWord( $mw ), "should not have matched magic word" );
 +      }
 +
 +      public function testUpdateRedirect( ) {
 +              $target = Title::newFromText( "testUpdateRedirect_target" );
 +
 +              // test with non-redirect page
 +              $content = $this->newContent( "hello world." );
 +              $newContent = $content->updateRedirect( $target );
 +
 +              $this->assertTrue( $content->equals( $newContent ), "content should be unchanged" );
 +
 +              // test with actual redirect
 +              $content = $this->newContent( "#REDIRECT [[Someplace]]" );
 +              $newContent = $content->updateRedirect( $target );
 +
 +              $this->assertFalse( $content->equals( $newContent ), "content should have changed" );
 +              $this->assertTrue( $newContent->isRedirect(), "new content should be a redirect" );
 +
 +              $this->assertEquals( $target->getFullText(), $newContent->getRedirectTarget()->getFullText() );
 +      }
 +
 +      # =================================================================================================================
 +
 +      public function testGetModel() {
 +              $content = $this->newContent( "hello world." );
 +
 +              $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getModel() );
 +      }
 +
 +      public function testGetContentHandler() {
 +              $content = $this->newContent( "hello world." );
 +
 +              $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getContentHandler()->getModelID() );
 +      }
 +
 +      public function dataIsEmpty( ) {
 +              return array(
 +                      array( '', true ),
 +                      array( '  ', false ),
 +                      array( '0', false ),
 +                      array( 'hallo welt.', false ),
 +              );
 +      }
 +
 +      /**
 +       * @dataProvider dataIsEmpty
 +       */
 +      public function testIsEmpty( $text, $empty ) {
 +              $content = $this->newContent( $text );
 +
 +              $this->assertEquals( $empty, $content->isEmpty() );
 +      }
 +
 +      public function dataEquals( ) {
 +              return array(
 +                      array( new WikitextContent( "hallo" ), null, false ),
 +                      array( new WikitextContent( "hallo" ), new WikitextContent( "hallo" ), true ),
 +                      array( new WikitextContent( "hallo" ), new JavascriptContent( "hallo" ), false ),
 +                      array( new WikitextContent( "hallo" ), new WikitextContent( "HALLO" ), false ),
 +              );
 +      }
 +
 +      /**
 +       * @dataProvider dataEquals
 +       */
 +      public function testEquals( Content $a, Content $b = null, $equal = false ) {
 +              $this->assertEquals( $equal, $a->equals( $b ) );
 +      }
 +
 +      public function dataGetDeletionUpdates() {
 +              return array(
 +                      array("WikitextContentTest_testGetSecondaryDataUpdates_1", "hello ''world''\n",
 +                              array( 'LinksDeletionUpdate' => array( ) )
 +                      ),
 +                      array("WikitextContentTest_testGetSecondaryDataUpdates_2", "hello [[world test 21344]]\n",
 +                              array( 'LinksDeletionUpdate' => array( ) )
 +                      ),
 +                      // @todo: more...?
 +              );
 +      }
 +
 +      /**
 +       * @dataProvider dataGetDeletionUpdates
 +       */
 +      public function testDeletionUpdates( $title, $text, $expectedStuff ) {
 +              $title = Title::newFromText( $title );
 +              $title->resetArticleID( 2342 ); //dummy id. fine as long as we don't try to execute the updates!
 +
 +              $handler = ContentHandler::getForModelID( $title->getContentModel() );
 +              $content = ContentHandler::makeContent( $text, $title );
 +
++              $updates = $content->getDeletionUpdates( WikiPage::factory( $title ) );
 +
 +              // make updates accessible by class name
 +              foreach ( $updates as $update ) {
 +                      $class = get_class( $update );
 +                      $updates[ $class ] = $update;
 +              }
 +
 +              foreach ( $expectedStuff as $class => $fieldValues ) {
 +                      $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" );
 +
 +                      $update = $updates[ $class ];
 +
 +                      foreach ( $fieldValues as $field => $value ) {
 +                              $v = $update->$field; #if the field doesn't exist, just crash and burn
 +                              $this->assertEquals( $value, $v, "unexpected value for field $field in instance of $class" );
 +                      }
 +              }
 +      }
 +
 +}
@@@ -250,9 -250,9 +250,9 @@@ class TextPassDumperTest extends DumpTe
                        $dumper->stderr = $stderr;
  
                        // The actual dump and taking time
-                       $ts_before = wfTime();
+                       $ts_before = microtime( true );
                        $dumper->dump( WikiExporter::FULL, WikiExporter::TEXT );
-                       $ts_after = wfTime();
+                       $ts_after = microtime( true );
                        $lastDuration = $ts_after - $ts_before;
  
                        // Handling increasing the iteration count for the stubs
                $this->assertEmpty( $files, "Remaining unchecked files" );
  
                // ... and have dealt with more than one checkpoint file
 -              $this->assertGreaterThan( 1, $checkpointFiles, "# of checkpoint files" );
 +              $this->assertGreaterThan( 1, $checkpointFiles, "expected more than 1 checkpoint to have been created. Checkpoint interval is $checkpointAfter seconds, maybe your computer is too fast?" );
  
                $this->expectETAOutput();
        }
        </contributor>
        <comment>BackupDumperTestP1Summary1</comment>
        <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
 +      <model>wikitext</model>
 +      <format>text/x-wiki</format>
        <text id="' . $this->textId1_1 . '" bytes="23" />
      </revision>
    </page>
        </contributor>
        <comment>BackupDumperTestP2Summary1</comment>
        <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
 +      <model>wikitext</model>
 +      <format>text/x-wiki</format>
        <text id="' . $this->textId2_1 . '" bytes="23" />
      </revision>
      <revision>
        </contributor>
        <comment>BackupDumperTestP2Summary2</comment>
        <sha1>b7vj5ks32po5m1z1t1br4o7scdwwy95</sha1>
 +      <model>wikitext</model>
 +      <format>text/x-wiki</format>
        <text id="' . $this->textId2_2 . '" bytes="23" />
      </revision>
      <revision>
        </contributor>
        <comment>BackupDumperTestP2Summary3</comment>
        <sha1>jfunqmh1ssfb8rs43r19w98k28gg56r</sha1>
 +      <model>wikitext</model>
 +      <format>text/x-wiki</format>
        <text id="' . $this->textId2_3 . '" bytes="23" />
      </revision>
      <revision>
        </contributor>
        <comment>BackupDumperTestP2Summary4 extra</comment>
        <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
 +      <model>wikitext</model>
 +      <format>text/x-wiki</format>
        <text id="' . $this->textId2_4 . '" bytes="44" />
      </revision>
    </page>
        </contributor>
        <comment>Talk BackupDumperTestP1 Summary1</comment>
        <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
 +      <model>wikitext</model>
 +      <format>text/x-wiki</format>
        <text id="' . $this->textId4_1 . '" bytes="35" />
      </revision>
    </page>