merged master
authordaniel <daniel.kinzler@wikimedia.de>
Tue, 18 Sep 2012 15:43:20 +0000 (17:43 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Wed, 19 Sep 2012 10:34:10 +0000 (12:34 +0200)
Change-Id: I0ef7c7f33a5dc5855f38b20c03ddc5306f38ec66

21 files changed:
1  2 
docs/hooks.txt
includes/Article.php
includes/DefaultSettings.php
includes/EditPage.php
includes/GlobalFunctions.php
includes/ImagePage.php
includes/Revision.php
includes/Title.php
includes/WikiPage.php
includes/api/ApiParse.php
includes/diff/DifferenceEngine.php
includes/parser/Parser.php
includes/specials/SpecialUndelete.php
languages/Language.php
languages/messages/MessagesDe.php
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
maintenance/Maintenance.php
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/RevisionStorageTest.php
thumb.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
@@@ -325,12 -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
+ '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
+ &$message: API usage message to die with, as a message key or array
+ as accepted by ApiBase::dieUsageMsg.
  
  'APIEditBeforeSave': before saving a page with api.php?action=edit,
  after processing request parameters. Return false to let the request
@@@ -433,14 -429,9 +434,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
@@@ -488,7 -479,7 +489,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
@@@ -496,18 -487,7 +497,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
@@@ -558,7 -538,7 +559,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
@@@ -567,16 -547,7 +568,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
@@@ -584,22 -555,9 +585,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
@@@ -628,19 -586,11 +629,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
@@@ -772,16 -722,6 +773,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
@@@ -855,19 -795,12 +856,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
@@@ -878,7 -811,7 +879,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
@@@ -930,28 -863,14 +931,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
@@@ -1811,8 -1730,7 +1812,8 @@@ $title : Current Title object being dis
  '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
@@@ -2431,14 -2349,6 +2432,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();
 +              //XXX: this isn't page content but a UI message. horrible.
 +              $this->mContentObject = new MessageContent( 'missing-revision', array( $oldid ), array() ) ;
  
                if ( $oldid ) {
                        # $this->mRevision might already be fetched by getOldIDFromRequest()
                        }
  
                        $this->mRevision = $this->mPage->getRevision();
 +
                        if ( !$this->mRevision ) {
                                wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" );
                                wfProfileOut( __METHOD__ );
  
                // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks.
                // We should instead work with the Revision object when we need it...
 -              $this->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() );
                }
        }
  
                // 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 ) {
-               $user = is_null( $user ) ? $this->getContext()->getUser() : $user;
-               $parserOptions = $this->mPage->makeParserOptions( $user );
 +              //XXX: bypasses mParserOptions and thus setParserOptions()
 +
+               if ( $user === null ) {
+                       $parserOptions = $this->getParserOptions();
+               } else {
+                       $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
         */
        public function getParserOptions() {
                if ( !$this->mParserOptions ) {
-                       $this->mParserOptions = $this->mPage->makeParserOptions( $this->getContext()->getUser() );
+                       $this->mParserOptions = $this->mPage->makeParserOptions( $this->getContext() );
                }
                // Clone to allow modifications of the return value without affecting cache
                return clone $this->mParserOptions;
         * @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 );
@@@ -732,16 -732,6 +732,16 @@@ $wgMediaHandlers = array
        'image/x-djvu'   => 'DjVuHandler', // compat
  );
  
 +/**
 + * Plugins for page content model handling.
 + * Each entry in the array maps a model id to a class name
 + */
 +$wgContentHandlers = array(
 +      CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', // the usual case
 +      CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', // dumb version, no syntax highlighting
 +      CONTENT_MODEL_CSS => 'CssContentHandler', // dumb version, no syntax highlighting
 +);
 +
  /**
   * Resizing can be done using PHP's internal image libraries or using
   * ImageMagick or another third-party converter, e.g. GraphicMagick.
@@@ -2721,7 -2711,7 +2721,7 @@@ $wgFooterIcons = array
   *  - true = use a combined login / create account link
   *  - false = split login and create account into two separate links
   */
- $wgUseCombinedLoginLink = true;
+ $wgUseCombinedLoginLink = false;
  
  /**
   * Search form behavior for Vector skin only.
@@@ -3228,10 -3218,18 +3228,18 @@@ $wgParserConf = array
  $wgMaxTocLevel = 999;
  
  /**
-  * A complexity limit on template expansion
+  * A complexity limit on template expansion: the maximum number of nodes visited
+  * by PPFrame::expand()
   */
  $wgMaxPPNodeCount = 1000000;
  
+ /**
+  * A complexity limit on template expansion: the maximum number of nodes 
+  * generated by Preprocessor::preprocessToObj()
+  */
+ $wgMaxGeneratedPPNodeCount = 1000000;
  /**
   * Maximum recursion depth for templates within templates.
   * The current parser adds two levels to the PHP call stack for each template,
@@@ -6225,31 -6223,6 +6233,31 @@@ $wgSeleniumConfigFile = null
  $wgDBtestuser = ''; //db user that has permission to create and drop the test databases only
  $wgDBtestpassword = '';
  
 +/**
 + * Associative array mapping namespace IDs to the name of the content model pages in that namespace should have by
 + * default (use the CONTENT_MODEL_XXX constants). If no special content type is defined for a given namespace,
 + * pages in that namespace will  use the CONTENT_MODEL_WIKITEXT (except for the special case of JS and CS pages).
 + */
 +$wgNamespaceContentModels = array();
 +
 +/**
 + * How to react if a plain text version of a non-text Content object is requested using ContentHandler::getContentText():
 + *
 + * * 'ignore': return null
 + * * 'fail': throw an MWException
 + * * 'serializeContent': serializeContent to default format
 + */
 +$wgContentHandlerTextFallback = 'ignore';
 +
 +/**
 + * Compatibility switch for running ContentHandler code withoput a schema update.
 + * Set to false to disable use of the database fields introduced by the ContentHandler facility.
 + *
 + * @deprecated this is only here to allow code deployment without a database schema update on large sites.
 + *             get rid of it in the next version.
 + */
 +$wgContentHandlerUseDB = true;
 +
  /**
   * Whether the user must enter their password to change their e-mail address
   *
diff --combined includes/EditPage.php
@@@ -155,11 -155,6 +155,11 @@@ class EditPage 
         */
        const AS_IMAGE_REDIRECT_LOGGED     = 234;
  
 +      /**
 +       * Status: can't parse content
 +       */
 +      const AS_PARSE_ERROR                = 240;
 +
        /**
         * HTML id and name for the beginning of the edit form.
         */
        var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false;
        var $edittime = '', $section = '', $sectiontitle = '', $starttime = '';
        var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true;
 +      var $content_model = null, $content_format = null;
  
        # Placeholders for text injection by hooks (must be HTML)
        # extensions should take care to _append_ to the present value
        public $editFormTextBottom = '';
        public $editFormTextAfterContent = '';
        public $previewTextAfterContent = '';
 -      public $mPreloadText = '';
 +      public $mPreloadContent = null;
  
        /* $didSave should be set to true whenever an article was succesfully altered. */
        public $didSave = false;
  
        public $suppressIntro = false;
  
 +      /**
 +       * Set to true to allow editing of non-text content types.
 +       *
 +       * @var bool
 +       */
 +      public $allowNonTextContent = false;
 +
        /**
         * @param $article Article
         */
        public function __construct( Article $article ) {
                $this->mArticle = $article;
                $this->mTitle = $article->getTitle();
 +
 +              $this->content_model = $this->mTitle->getContentModel();
 +
 +              $handler = ContentHandler::getForModelID( $this->content_model );
 +              $this->content_format = $handler->getDefaultFormat(); #NOTE: should be overridden by format of actual revision
        }
  
        /**
                        return;
                }
  
 -              $content = $this->getContent();
 +              $content = $this->getContentObject();
  
                # Use the normal message if there's nothing to display
 -              if ( $this->firsttime && $content === '' ) {
 +              if ( $this->firsttime && $content->isEmpty() ) {
                        $action = $this->mTitle->exists() ? 'edit' :
                                ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
                        throw new PermissionsError( $action, $permErrors );
                # If the user made changes, preserve them when showing the markup
                # (This happens when a user is blocked during edit, for instance)
                if ( !$this->firsttime ) {
 -                      $content = $this->textbox1;
 +                      $text = $this->textbox1;
                        $wgOut->addWikiMsg( 'viewyourtext' );
                } else {
 +                      $text = $this->toEditText( $content );
                        $wgOut->addWikiMsg( 'viewsourcetext' );
                }
  
 -              $this->showTextbox( $content, 'wpTextbox1', array( 'readonly' ) );
 +              $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) );
  
                $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ),
                        Linker::formatTemplates( $this->getTemplates() ) ) );
                        }
                }
  
 +              $this->oldid = $request->getInt( 'oldid' );
 +
                $this->bot = $request->getBool( 'bot', true );
                $this->nosummary = $request->getBool( 'nosummary' );
  
 -              $this->oldid = $request->getInt( 'oldid' );
 +              $content_handler = ContentHandler::getForTitle( $this->mTitle );
 +              $this->content_model = $request->getText( 'model', $content_handler->getModelID() ); #may be overridden by revision
 +              $this->content_format = $request->getText( 'format', $content_handler->getDefaultFormat() ); #may be overridden by revision
 +
 +              #TODO: check if the desired model is allowed in this namespace, and if a transition from the page's current model to the new model is allowed
 +              #TODO: check if the desired content model supports the given content format!
  
                $this->live = $request->getCheck( 'live' );
                $this->editintro = $request->getText( 'editintro',
        function initialiseForm() {
                global $wgUser;
                $this->edittime = $this->mArticle->getTimestamp();
 -              $this->textbox1 = $this->getContent( false );
 +
 +              $content = $this->getContentObject( false ); #TODO: track content object?!
 +              $this->textbox1 = $this->toEditText( $content );
 +
                // activate checkboxes if user wants them to be always active
                # Sort out the "watch" checkbox
                if ( $wgUser->getOption( 'watchdefault' ) ) {
         * @param $def_text string
         * @return mixed string on success, $def_text for invalid sections
         * @private
 +       * @deprecated since 1.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 = $this->toEditContent( $def_text );
 +              } else {
 +                      $def_content = false;
 +              }
 +
 +              $content = $this->getContentObject( $def_content );
 +
 +              // Note: EditPage should only be used with text based content anyway.
 +              return $this->toEditText( $content );
 +      }
 +
 +      private function getContentObject( $def_content = null ) {
 +              global $wgOut, $wgRequest;
  
                wfProfileIn( __METHOD__ );
  
 -              $text = false;
 +              $content = false;
  
                // For message page not locally set, use the i18n message.
                // For other non-existent articles, use preload text if any.
                if ( !$this->mTitle->exists() || $this->section == 'new' ) {
                        if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
                                # If this is a system message, get the default text.
 -                              $text = $this->mTitle->getDefaultMessageText();
 +                              $msg = $this->mTitle->getDefaultMessageText();
 +
 +                              $content = $this->toEditContent( $msg );
                        }
 -                      if ( $text === false ) {
 +                      if ( $content === false ) {
                                # If requested, preload some text.
                                $preload = $wgRequest->getVal( 'preload',
                                        // Custom preload text for new sections
                                        $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
 -                              $text = $this->getPreloadedText( $preload );
 +
 +                              $content = $this->getPreloadedContent( $preload );
                        }
                // For existing pages, get text based on "undo" or section parameters.
                } else {
                        if ( $this->section != '' ) {
                                // Get section edit text (returns $def_text for invalid sections)
 -                              $text = $wgParser->getSection( $this->getOriginalContent(), $this->section, $def_text );
 +                              $orig = $this->getOriginalContent();
 +                              $content = $orig ? $orig->getSection( $this->section ) : null;
 +
 +                              if ( !$content ) $content = $def_content;
                        } else {
                                $undoafter = $wgRequest->getInt( 'undoafter' );
                                $undo = $wgRequest->getInt( 'undo' );
  
                                        # Sanity check, make sure it's the right page,
                                        # the revisions exist and they were not deleted.
 -                                      # Otherwise, $text will be left as-is.
 +                                      # Otherwise, $content will be left as-is.
                                        if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
                                                $undorev->getPage() == $oldrev->getPage() &&
                                                $undorev->getPage() == $this->mTitle->getArticleID() &&
                                                !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
                                                !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
  
 -                                              $text = $this->mArticle->getUndoText( $undorev, $oldrev );
 -                                              if ( $text === false ) {
 +                                              $content = $this->mArticle->getUndoContent( $undorev, $oldrev );
 +
 +                                              if ( $content === false ) {
                                                        # Warn the user that something went wrong
                                                        $undoMsg = 'failure';
                                                } else {
                                                wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
                                }
  
 -                              if ( $text === false ) {
 -                                      $text = $this->getOriginalContent();
 +                              if ( $content === false ) {
 +                                      $content = $this->getOriginalContent();
                                }
                        }
                }
  
                wfProfileOut( __METHOD__ );
 -              return $text;
 +              return $content;
        }
  
        /**
         */
        private function getOriginalContent() {
                if ( $this->section == 'new' ) {
 -                      return $this->getCurrentText();
 +                      return $this->getCurrentContent();
                }
                $revision = $this->mArticle->getRevisionFetched();
                if ( $revision === null ) {
 -                      return '';
 +                      if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModel();
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +
 +                      return $handler->makeEmptyContent();
                }
 -              return $this->mArticle->getContent();
 +              $content = $revision->getContent();
 +              return $content;
        }
  
        /**
 -       * Get the actual text of the page. This is basically similar to
 -       * WikiPage::getRawText() except that when the page doesn't exist an empty
 -       * string is returned instead of false.
 +       * Get the current content of the page. This is basically similar to
 +       * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
 +       * content object is returned instead of null.
         *
 -       * @since 1.19
 +       * @since 1.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 = $this->toEditContent( $text );
 +
 +              $this->setPreloadedContent( $content );
 +      }
 +
 +      /**
 +       * Use this method before edit() to preload some content into the edit box
 +       *
 +       * @param $content Content
 +       *
 +       * @since 1.WD
 +       */
 +      public function setPreloadedContent( Content $content ) {
 +              $this->mPreloadedContent = $content;
        }
  
        /**
         * an earlier setPreloadText() or by loading the given page.
         *
         * @param $preload String: representing the title to preload from.
 +       *
         * @return String
 +       *
 +       * @deprecated since 1.WD, use getPreloadedContent() instead
         */
 -      protected function getPreloadedText( $preload ) {
 -              global $wgUser, $wgParser;
 +      protected function getPreloadedText( $preload ) { #NOTE: B/C only, replace usage!
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              $content = $this->getPreloadedContent( $preload );
 +              $text = $this->toEditText( $content );
 +
 +              return $text;
 +      }
  
 -              if ( !empty( $this->mPreloadText ) ) {
 -                      return $this->mPreloadText;
 +      /**
 +       * Get the contents to be preloaded into the box, either set by
 +       * an earlier setPreloadText() or by loading the given page.
 +       *
 +       * @param $preload String: representing the title to preload from.
 +       *
 +       * @return Content
 +       *
 +       * @since 1.WD
 +       */
 +      protected function getPreloadedContent( $preload ) { #@todo: use this!
 +              global $wgUser;
 +
 +              if ( !empty( $this->mPreloadContent ) ) {
 +                      return $this->mPreloadContent;
                }
  
 +              $handler = ContentHandler::getForTitle( $this->getTitle() );
 +
                if ( $preload === '' ) {
 -                      return '';
 +                      return $handler->makeEmptyContent();
                }
  
                $title = Title::newFromText( $preload );
                # Check for existence to avoid getting MediaWiki:Noarticletext
                if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
 -                      return '';
 +                      return $handler->makeEmptyContent();
                }
  
                $page = WikiPage::factory( $title );
                        $title = $page->getRedirectTarget();
                        # Same as before
                        if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
 -                              return '';
 +                              return $handler->makeEmptyContent();
                        }
                        $page = WikiPage::factory( $title );
                }
  
                $parserOptions = ParserOptions::newFromUser( $wgUser );
 -              return $wgParser->getPreloadText( $page->getRawText(), $title, $parserOptions );
 +              $content = $page->getContent( Revision::RAW );
 +
 +              return $content->preloadTransform( $title, $parserOptions );
        }
  
        /**
                        case self::AS_HOOK_ERROR:
                                return false;
  
 +                      case self::AS_PARSE_ERROR:
 +                              $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>');
 +                              return true;
 +
                        case self::AS_SUCCESS_NEW_ARTICLE:
                                $query = $resultDetails['redirect'] ? 'redirect=no' : '';
                                $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
                        return $status;
                }
  
 +              try {
 +                      # Construct Content object
 +                      $textbox_content = $this->toEditContent( $this->textbox1 );
 +              } catch (MWContentSerializationException $ex) {
 +                      $status->fatal( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $status->value = self::AS_PARSE_ERROR;
 +                      wfProfileOut( __METHOD__ );
 +                      return $status;
 +              }
 +
                # Check image redirect
                if ( $this->mTitle->getNamespace() == NS_FILE &&
 -                      Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
 +                      $textbox_content->isRedirect() &&
                        !$wgUser->isAllowed( 'upload' ) ) {
                                $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
                                $status->setResult( false, $code );
                                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 ) {
                                                $this->isConflict = false;
                                                wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
                                        }
-                               } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) {
+                               } elseif ( $this->section == '' && Revision::userWasLastToEdit( DB_MASTER,  $this->mTitle->getArticleID(), $wgUser->getId(), $this->edittime ) ) {
                                        # Suppress edit conflict with self, except for section edits where merging is required.
                                        wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
                                        $this->isConflict = false;
                                $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 = $this->toEditText( $content );
                        $this->section = '';
  
                        $status->value = self::AS_SUCCESS_UPDATE;
                }
  
                // Check for length errors again now that the section is merged in
 -              $this->kblength = (int)( strlen( $text ) / 1024 );
 +                      $this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 );
                if ( $this->kblength > $wgMaxArticleSize ) {
                        $this->tooBig = true;
                        $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
                        ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
                        ( $bot ? EDIT_FORCE_BOT : 0 );
  
 -              $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags );
 +                      $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags,
 +                                                                                                                      false, null, $this->content_format );
  
                if ( $doEditStatus->isOK() ) {
 -                      $result['redirect'] = Title::newFromRedirect( $text ) !== null;
 +                              $result['redirect'] = $content->isRedirect();
                        $this->commitWatch();
                        wfProfileOut( __METHOD__ );
                        return $status;
                }
        }
  
-       /**
-        * Check if no edits were made by other users since
-        * the time a user started editing the page. Limit to
-        * 50 revisions for the sake of performance.
-        *
-        * @param $id int
-        * @param $edittime string
-        *
-        * @return bool
-        */
-       protected function userWasLastToEdit( $id, $edittime ) {
-               if ( !$id ) return false;
-               $dbw = wfGetDB( DB_MASTER );
-               $res = $dbw->select( 'revision',
-                       'rev_user',
-                       array(
-                               'rev_page' => $this->mTitle->getArticleID(),
-                               'rev_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $edittime ) )
-                       ),
-                       __METHOD__,
-                       array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) );
-               foreach ( $res as $row ) {
-                       if ( $row->rev_user != $id ) {
-                               return false;
-                       }
-               }
-               return true;
-       }
        /**
         * @private
         * @todo document
         * @param $editText string
         *
         * @return bool
 +       * @deprecated since 1.WD, use mergeChangesIntoContent() instead
         */
 -      function mergeChangesInto( &$editText ) {
 +      function mergeChangesInto( &$editText ){
 +              wfDebug( __METHOD__, "1.WD" );
 +
 +              $editContent = $this->toEditContent( $editText );
 +
 +              $ok = $this->mergeChangesIntoContent( $editContent );
 +
 +              if ( $ok ) {
 +                      $editText = $this->toEditText( $editContent );
 +                      return true;
 +              } else {
 +                      return false;
 +              }
 +      }
 +
 +      /**
 +       * @private
 +       * @todo document
 +       *
 +       * @parma $editText string
 +       *
 +       * @return bool
 +       * @since since 1.WD
 +       */
 +      private function mergeChangesIntoContent( &$editContent ){
                wfProfileIn( __METHOD__ );
  
                $db = wfGetDB( DB_MASTER );
                        wfProfileOut( __METHOD__ );
                        return false;
                }
 -              $baseText = $baseRevision->getText();
 +              $baseContent = $baseRevision->getContent();
  
                // The current state, we want to merge updates into it
                $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
                        wfProfileOut( __METHOD__ );
                        return false;
                }
 -              $currentText = $currentRevision->getText();
 +              $currentContent = $currentRevision->getContent();
 +
 +              $handler = ContentHandler::getForModelID( $baseContent->getModel() );
 +
 +              $result = $handler->merge3( $baseContent, $editContent, $currentContent );
  
 -              $result = '';
 -              if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
 -                      $editText = $result;
 +              if ( $result ) {
 +                      $editContent = $result;
                        wfProfileOut( __METHOD__ );
                        return true;
                } else {
                }
        }
  
 +      /**
 +       * Gets an editable textual representation of the given Content object.
 +       * The textual representation can be turned by into a Content object by the
 +       * toEditContent() method.
 +       *
 +       * If the given Content object is not of a type that can be edited using the text base EditPage,
 +       * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual
 +       * content.
 +       *
 +       * @param Content $content
 +       * @return String the editable text form of the content.
 +       *
 +       * @throws MWException if $content is not an instance of TextContent and $this->allowNonTextContent is not true.
 +       */
 +      protected function toEditText( Content $content ) {
 +              if ( !$this->allowNonTextContent && !( $content instanceof TextContent ) ) {
 +                      throw new MWException( "This content model can not be edited as text: "
 +                                                              . ContentHandler::getLocalizedName( $content->getModel() ) );
 +              }
 +
 +              return $content->serialize( $this->content_format );
 +      }
 +
 +      /**
 +       * Turns the given text into a Content object by unserializing it.
 +       *
 +       * If the resulting Content object is not of a type that can be edited using the text base EditPage,
 +       * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual
 +       * content.
 +       *
 +       * @param String $text Text to unserialize
 +       * @return Content the content object created from $text
 +       *
 +       * @throws MWException if unserializing the text results in a Content object that is not an instance of TextContent
 +       *          and $this->allowNonTextContent is not true.
 +       */
 +      protected function toEditContent( $text ) {
 +              $content = ContentHandler::makeContent( $text, $this->getTitle(),
 +                      $this->content_model, $this->content_format );
 +
 +              if ( !$this->allowNonTextContent && !( $content instanceof TextContent ) ) {
 +                      throw new MWException( "This content model can not be edited as text: "
 +                              . ContentHandler::getLocalizedName( $content->getModel() ) );
 +              }
 +
 +              return $content;
 +      }
 +
        /**
         * Send the edit form and related headers to $wgOut
         * @param $formCallback Callback that takes an OutputPage parameter; will be called
                        }
                }
  
 +              //@todo: add EditForm plugin interface and use it here!
 +              //       search for textarea1 and textares2, and allow EditForm to override all uses.
                $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID,
                        'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ),
                        'enctype' => 'multipart/form-data' ) ) );
  
                $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
  
 +              $wgOut->addHTML( Html::hidden( 'format', $this->content_format ) );
 +              $wgOut->addHTML( Html::hidden( 'model', $this->content_model ) );
 +
                if ( $this->section == 'new' ) {
                        $this->showSummaryInput( true, $this->summary );
                        $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
                        // resolved between page source edits and custom ui edits using the
                        // custom edit ui.
                        $this->textbox2 = $this->textbox1;
 -                      $this->textbox1 = $this->getCurrentText();
 +
 +                      $content = $this->getCurrentContent();
 +                      $this->textbox1 = $this->toEditText( $content );
  
                        $this->showTextbox1();
                } else {
                        Linker::formatHiddenCategories( $this->mArticle->getHiddenCategories() ) ) );
  
                if ( $this->isConflict ) {
 -                      $this->showConflict();
 +                      try {
 +                              $this->showConflict();
 +                      } catch ( MWContentSerializationException $ex ) {
 +                              // this can't really happen, but be nice if it does.
 +                              $msg = wfMessage( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                              $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>');
 +                      }
                }
  
                $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
  
                        if ( $this->section != '' && $this->section != 'new' ) {
                                if ( !$this->summary && !$this->preview && !$this->diff ) {
 -                                      $sectionTitle = self::extractSectionTitle( $this->textbox1 );
 +                                      $sectionTitle = self::extractSectionTitle( $this->textbox1 ); //FIXME: use Content object
                                        if ( $sectionTitle !== false ) {
                                                $this->summary = "/* $sectionTitle */ ";
                                        }
                $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 = $this->toEditContent( $oldtext );
 +                      } else {
 +                              $oldContent = null;
                        }
                } else {
 -                      $oldtext = $this->mArticle->getRawText();
 +                      $oldContent = $this->getOriginalContent();
                }
 -              $newtext = $this->mArticle->replaceSection(
 -                      $this->section, $this->textbox1, $this->summary, $this->edittime );
  
 -              wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
 +              $textboxContent = $this->toEditContent( $this->textbox1 );
 +
 +              $newContent = $this->mArticle->replaceSectionContent(
 +                                                                                      $this->section, $textboxContent,
 +                                                                                      $this->summary, $this->edittime );
 +
 +              ContentHandler::runLegacyHooks( 'EditPageGetDiffText', array( $this, &$newContent ) );
 +              wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) );
  
                $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
 -              $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts );
 +              $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
  
 -              if ( $oldtext !== false  || $newtext != '' ) {
 +              if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
                        $oldtitle = wfMessage( $oldtitlemsg )->parse();
                        $newtitle = wfMessage( 'yourtext' )->parse();
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $oldtext, $newtext );
 +                      $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $oldContent, $newContent );
 +
                        $difftext = $de->getDiff( $oldtitle, $newtitle );
                        $de->showDiffStyle();
                } else {
                if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $this->textbox2, $this->textbox1 );
 -                      $de->showDiff(
 +                      $content1 = $this->toEditContent( $this->textbox1 );
 +                      $content2 = $this->toEditContent( $this->textbox2 );
 +
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +                      $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $content2, $content1 );
 +                      $de->showDiff( 
                                wfMessage( 'yourtext' )->parse(),
                                wfMessage( 'storedversion' )->text()
                        );
                        return $parsedNote;
                }
  
 -              if ( $this->mTriedSave && !$this->mTokenOk ) {
 -                      if ( $this->mTokenOkExceptSuffix ) {
 -                              $note = wfMessage( 'token_suffix_mismatch' )->plain();
 -                      } else {
 -                              $note = wfMessage( 'session_fail_preview' )->plain();
 -                      }
 -              } elseif ( $this->incompleteForm ) {
 -                      $note = wfMessage( 'edit_form_incomplete' )->plain();
 -              } else {
 -                      $note = wfMessage( 'previewnote' )->plain() .
 -                              ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]';
 -              }
 +              $note = '';
  
 -              $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
 +              try {
 +                      $content = $this->toEditContent( $this->textbox1 );
  
 -              $parserOptions->setEditSection( false );
 -              $parserOptions->setIsPreview( true );
 -              $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
 +                      if ( $this->mTriedSave && !$this->mTokenOk ) {
 +                              if ( $this->mTokenOkExceptSuffix ) {
 +                                      $note = wfMessage( 'token_suffix_mismatch' )->plain() ;
 -              # don't parse non-wikitext pages, show message about preview
 -              if ( $this->mTitle->isCssJsSubpage() || !$this->mTitle->isWikitextPage() ) {
 -                      if ( $this->mTitle->isCssJsSubpage() ) {
 -                              $level = 'user';
 -                      } elseif ( $this->mTitle->isCssOrJsPage() ) {
 -                              $level = 'site';
 -                      } else {
 -                              $level = false;
 -                      }
 -
 -                      # Used messages to make sure grep find them:
 -                      # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 -                      $class = 'mw-code';
 -                      if ( $level ) {
 -                              if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMessage( "{$level}csspreview" )->text() . "\n</div>";
 -                                      $class .= " mw-css";
 -                              } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMessage( "{$level}jspreview" )->text() . "\n</div>";
 -                                      $class .= " mw-js";
                                } else {
 -                                      throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' );
 +                                      $note = wfMessage( 'session_fail_preview' )->plain() ;
                                }
 -                              $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
 -                              $previewHTML = $parserOutput->getText();
 +                      } elseif ( $this->incompleteForm ) {
 +                              $note = wfMessage( 'edit_form_incomplete' )->plain() ;
                        } else {
 -                              $previewHTML = '';
 -                      }
 +                              $note = wfMessage( 'previewnote' )->plain() .
 +                                      ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]';
 +                      }
 +
-                       $parserOptions = ParserOptions::newFromUser( $wgUser );
++                      $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
 +                      $parserOptions->setEditSection( false );
 +                      $parserOptions->setTidy( true );
 +                      $parserOptions->setIsPreview( true );
 +                      $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' );
 +
 +                      # don't parse non-wikitext pages, show message about preview
 +                      if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
 +                              if( $this->mTitle->isCssJsSubpage() ) {
 +                                      $level = 'user';
 +                              } elseif( $this->mTitle->isCssOrJsPage() ) {
 +                                      $level = 'site';
 +                              } else {
 +                                      $level = false;
 +                              }
  
 -                      $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n";
 -              } else {
 -                      $toparse = $this->textbox1;
 +                              if ( $content->getModel() == CONTENT_MODEL_CSS ) {
 +                                      $format = 'css';
 +                              } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
 +                                      $format = 'js';
 +                              } else {
 +                                      $format = false;
 +                              }
  
 -                      # If we're adding a comment, we need to show the
 -                      # summary as the headline
 -                      if ( $this->section == "new" && $this->summary != "" ) {
 -                              $toparse = wfMessage( 'newsectionheaderdefaultlevel', $this->summary )->inContentLanguage()->text() . "\n\n" . $toparse;
 +                              # Used messages to make sure grep find them:
 +                              # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 +                              if( $level && $format ) {
 +                                      $note = "<div id='mw-{$level}{$format}preview'>" . wfMessage( "{$level}{$format}preview" )->text()  . "</div>";
 +                              } else {
 +                                      $note = wfMessage( 'previewnote' )->text() ;
 +                              }
 +                      } else {
 +                              $note = wfMessage( 'previewnote' )->text() ;
                        }
  
 -                      wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
 -
 -                      $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
 -                      $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
 -
 -                      $rt = Title::newFromRedirectArray( $this->textbox1 );
 +                      $rt = $content->getRedirectChain();
                        if ( $rt ) {
                                $previewHTML = $this->mArticle->viewRedirect( $rt, false );
                        } else {
 -                              $previewHTML = $parserOutput->getText();
 -                      }
  
 -                      $this->mParserOutput = $parserOutput;
 -                      $wgOut->addParserOutputNoText( $parserOutput );
 +                              # If we're adding a comment, we need to show the
 +                              # summary as the headline
 +                              if ( $this->section == "new" && $this->summary != "" ) {
 +                                      $content = $content->addSectionHeader( $this->summary );
 +                              }
 +
 +                              $hook_args = array( $this, &$content );
 +                              ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
 +                              wfRunHooks( 'EditPageGetPreviewContent', $hook_args );
 +
 +                              $parserOptions->enableLimitReport();
 +
 +                              # For CSS/JS pages, we should have called the ShowRawCssJs hook here.
 +                              # But it's now deprecated, so never mind
  
 -                      if ( count( $parserOutput->getWarnings() ) ) {
 -                              $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                              $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
 +                              $parserOutput = $content->getParserOutput( $this->getArticle()->getTitle(), null, $parserOptions );
 +
 +                              $previewHTML = $parserOutput->getText();
 +                              $this->mParserOutput = $parserOutput;
 +                              $wgOut->addParserOutputNoText( $parserOutput );
 +
 +                              if ( count( $parserOutput->getWarnings() ) ) {
 +                                      $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                              }
                        }
 +              } catch (MWContentSerializationException $ex) {
 +                      $m = wfMessage('content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $note .= "\n\n" . $m->parse();
 +                      $previewHTML = '';
                }
  
                if ( $this->isConflict ) {
@@@ -391,7 -391,7 +391,7 @@@ function wfArrayToCgi( $array1, $array
  
        $cgi = '';
        foreach ( $array1 as $key => $value ) {
 -              if ( !is_null($value) && $value !== false ) {
 +              if ( $value !== false ) {
                        if ( $cgi != '' ) {
                                $cgi .= '&';
                        }
                        } else {
                                if ( is_object( $value ) ) {
                                        $value = $value->__toString();
 +                              } elseif( !is_null( $value ) ) {
 +                                      $cgi .= urlencode( $key ) . '=' . urlencode( $value );
 +                              } else {
 +                                      $cgi .= urlencode( $key );
                                }
 -                              $cgi .= urlencode( $key ) . '=' . urlencode( $value );
                        }
                }
        }
@@@ -443,15 -440,14 +443,15 @@@ function wfCgiToArray( $query ) 
                        continue;
                }
                if ( strpos( $bit, '=' ) === false ) {
 -                      // Pieces like &qwerty become 'qwerty' => '' (at least this is what php does)
 -                      $key = $bit;
 -                      $value = '';
 +                      // Pieces like &qwerty become 'qwerty' => null
 +                      $key = urldecode( $bit );
 +                      $value = null;
                } else {
                        list( $key, $value ) = explode( '=', $bit );
 +                      $key = urldecode( $key );
 +                      $value = urldecode( $value );
                }
 -              $key = urldecode( $key );
 -              $value = urldecode( $value );
 +
                if ( strpos( $key, '[' ) !== false ) {
                        $keys = array_reverse( explode( '[', $key ) );
                        $key = array_pop( $keys );
   * Append a query string to an existing URL, which may or may not already
   * have query string parameters already. If so, they will be combined.
   *
 + * @deprecated in 1.20. Use Uri class.
   * @param $url String
   * @param $query Mixed: string or associative array
   * @return string
   */
  function wfAppendQuery( $url, $query ) {
 -      if ( is_array( $query ) ) {
 -              $query = wfArrayToCgi( $query );
 -      }
 -      if( $query != '' ) {
 -              if( false === strpos( $url, '?' ) ) {
 -                      $url .= '?';
 -              } else {
 -                      $url .= '&';
 -              }
 -              $url .= $query;
 -      }
 -      return $url;
 +      $obj = new Uri( $url );
 +      $obj->extendQuery( $query );
 +      return $obj->toString();
  }
  
  /**
@@@ -572,13 -576,49 +572,13 @@@ function wfExpandUrl( $url, $defaultPro
   * @todo Need to integrate this into wfExpandUrl (bug 32168)
   *
   * @since 1.19
 + * @deprecated
   * @param $urlParts Array URL parts, as output from wfParseUrl
   * @return string URL assembled from its component parts
   */
  function wfAssembleUrl( $urlParts ) {
 -      $result = '';
 -
 -      if ( isset( $urlParts['delimiter'] ) ) {
 -              if ( isset( $urlParts['scheme'] ) ) {
 -                      $result .= $urlParts['scheme'];
 -              }
 -
 -              $result .= $urlParts['delimiter'];
 -      }
 -
 -      if ( isset( $urlParts['host'] ) ) {
 -              if ( isset( $urlParts['user'] ) ) {
 -                      $result .= $urlParts['user'];
 -                      if ( isset( $urlParts['pass'] ) ) {
 -                              $result .= ':' . $urlParts['pass'];
 -                      }
 -                      $result .= '@';
 -              }
 -
 -              $result .= $urlParts['host'];
 -
 -              if ( isset( $urlParts['port'] ) ) {
 -                      $result .= ':' . $urlParts['port'];
 -              }
 -      }
 -
 -      if ( isset( $urlParts['path'] ) ) {
 -              $result .= $urlParts['path'];
 -      }
 -
 -      if ( isset( $urlParts['query'] ) ) {
 -              $result .= '?' . $urlParts['query'];
 -      }
 -
 -      if ( isset( $urlParts['fragment'] ) ) {
 -              $result .= '#' . $urlParts['fragment'];
 -      }
 -
 -      return $result;
 +      $obj = new Uri( $urlParts );
 +      return $obj->toString();
  }
  
  /**
@@@ -725,7 -765,6 +725,7 @@@ function wfUrlProtocolsWithoutProtRel(
   * 2) Handles protocols that don't use :// (e.g., mailto: and news: , as well as protocol-relative URLs) correctly
   * 3) Adds a "delimiter" element to the array, either '://', ':' or '//' (see (2))
   *
 + * @deprecated
   * @param $url String: a URL to parse
   * @return Array: bits of the URL in an associative array, per PHP docs
   */
@@@ -1025,7 -1064,6 +1025,7 @@@ function wfLogDBError( $text ) 
                } else {
                        $d = date_create( "now", $logDBErrorTimeZoneObject );
                }
 +              $date = $d->format( 'D M j G:i:s T Y' );
  
                $date = $d->format( 'D M j G:i:s T Y' );
  
@@@ -2364,7 -2402,6 +2364,7 @@@ define( 'TS_ISO_8601_BASIC', 9 )
  /**
   * Get a timestamp string in one of various formats
   *
 + * @deprecated
   * @param $outputtype Mixed: A timestamp in one of the supported formats, the
   *                    function will autodetect which format is supplied and act
   *                    accordingly.
   * @return Mixed: String / false The same date in the format specified in $outputtype or false
   */
  function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) {
-       $timestamp = new MWTimestamp( $ts );
-       return $timestamp->getTimestamp( $outputtype );
+       try {
+               $timestamp = new MWTimestamp( $ts );
+               return $timestamp->getTimestamp( $outputtype );
+       } catch( TimestampException $e ) {
+               wfDebug("wfTimestamp() fed bogus time value: TYPE=$outputtype; VALUE=$ts\n");
+               return false;
+       }
  }
  
  /**
diff --combined includes/ImagePage.php
@@@ -157,9 -157,7 +157,9 @@@ class ImagePage extends Article 
                        $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
        }
  
        /**
 -       * 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() {
                                        # Some sort of audio file that doesn't have dimensions
                                        # Don't output a no hi res message for such a file
                                        $msgsmall = '';
+                               } elseif ( $this->displayImg->isVectorized() ) {
+                                       # For vectorized images, full size is just the frame size
+                                       $msgsmall = '';
                                } else {
                                        # Image is small enough to show full size on image page
                                        $msgsmall = wfMessage( 'file-nohires' )->parse();
diff --combined includes/Revision.php
@@@ -40,10 -40,6 +40,10 @@@ class Revision implements IDBAccessObje
        protected $mTextRow;
        protected $mTitle;
        protected $mCurrent;
 +      protected $mContentModel;
 +      protected $mContentFormat;
 +      protected $mContent;
 +      protected $mContentHandler;
  
        // Revision deletion constants
        const DELETED_TEXT = 1;
         * @return Revision
         */
        public static function newFromArchiveRow( $row, $overrides = array() ) {
 +              global $wgContentHandlerUseDB;
 +
                $attribs = $overrides + array(
                        'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
                        'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
                        'deleted'    => $row->ar_deleted,
                        'len'        => $row->ar_len,
                        'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
 +                      'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
 +                      'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
                );
 +
 +              if ( !$wgContentHandlerUseDB ) {
 +                      unset( $attribs['content_model'] );
 +                      unset( $attribs['content_format'] );
 +              }
 +
                if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
                        // Pre-1.5 ar_text row
                        $attribs['text'] = self::getRevisionText( $row, 'ar_' );
         * @return array
         */
        public static function selectFields() {
 -              return array(
 +              global $wgContentHandlerUseDB;
 +
 +              $fields = array(
                        'rev_id',
                        'rev_page',
                        'rev_text_id',
                        'rev_deleted',
                        'rev_len',
                        'rev_parent_id',
 -                      'rev_sha1'
 +                      'rev_sha1',
                );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'rev_content_format';
 +                      $fields[] = 'rev_content_model';
 +              }
 +
 +              return $fields;
        }
  
        /**
                                $this->mTitle = null;
                        }
  
 +                      if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
 +                              $this->mContentModel = null; # determine on demand if needed
 +                      } else {
 +                              $this->mContentModel = strval( $row->rev_content_model );
 +                      }
 +
 +                      if( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) {
 +                              $this->mContentFormat = null; # determine on demand if needed
 +                      } else {
 +                              $this->mContentFormat = strval( $row->rev_content_format );
 +                      }
 +
                        // Lazy extraction...
                        $this->mText      = null;
                        if( isset( $row->old_text ) ) {
                        // Build a new revision to be saved...
                        global $wgUser; // ugh
  
 +
 +                      # if we have a content object, use it to set the model and type
 +                      if ( !empty( $row['content'] ) ) {
 +                              //@todo: when is that set? test with external store setup! check out insertOn() [dk]
 +                              if ( !empty( $row['text_id'] ) ) {
 +                                      throw new MWException( "Text already stored in external store (id {$row['text_id']}), "
 +                                                                                      . "can't serialize content object" );
 +                              }
 +
 +                              $row['content_model'] = $row['content']->getModel();
 +                              # note: mContentFormat is initializes later accordingly
 +                              # note: content is serialized later in this method!
 +                              # also set text to null?
 +                      }
 +
                        $this->mId        = isset( $row['id']         ) ? intval( $row['id']         ) : null;
                        $this->mPage      = isset( $row['page']       ) ? intval( $row['page']       ) : null;
                        $this->mTextId    = isset( $row['text_id']    ) ? intval( $row['text_id']    ) : null;
                        $this->mParentId  = isset( $row['parent_id']  ) ? intval( $row['parent_id']  ) : null;
                        $this->mSha1      = isset( $row['sha1']  )      ? strval( $row['sha1']  )      : null;
  
 +                      $this->mContentModel = isset( $row['content_model']  )  ? strval( $row['content_model'] )  : null;
 +                      $this->mContentFormat    = isset( $row['content_format']  ) ? strval( $row['content_format'] ) : null;
 +
                        // Enforce spacing trimming on supplied text
                        $this->mComment   = isset( $row['comment']    ) ?  trim( strval( $row['comment'] ) ) : null;
                        $this->mText      = isset( $row['text']       ) ? rtrim( strval( $row['text']    ) ) : null;
                        $this->mTextRow   = null;
  
 -                      $this->mTitle     = null; # Load on demand if needed
 +                      $this->mTitle     = isset( $row['title']      ) ? $row['title'] : null;
 +
 +                      // if we have a Content object, override mText and mContentModel
 +                      if ( !empty( $row['content'] ) ) {
 +                              $handler = $this->getContentHandler();
 +                              $this->mContent = $row['content'];
 +
 +                              $this->mContentModel = $this->mContent->getModel();
 +                              $this->mContentHandler = null;
 +
 +                              $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
 +                      } elseif ( !is_null( $this->mText ) ) {
 +                              $handler = $this->getContentHandler();
 +                              $this->mContent = $handler->unserializeContent( $this->mText );
 +                      }
 +
 +                      // if we have a Title object, override mPage. Useful for testing and convenience.
 +                      if ( isset( $row['title'] ) ) {
 +                              $this->mTitle     = $row['title'];
 +                              $this->mPage      = $this->mTitle->getArticleID();
 +                      } else {
 +                              $this->mTitle     = null; // Load on demand if needed
 +                      }
 +
 +                      // @todo: XXX: really? we are about to create a revision. it will usually then be the current one.
                        $this->mCurrent   = false;
 -                      # If we still have no length, see it we have the text to figure it out
 +
 +                      // If we still have no length, see it we have the text to figure it out
                        if ( !$this->mSize ) {
 -                              $this->mSize = is_null( $this->mText ) ? null : strlen( $this->mText );
 +                              if ( !is_null( $this->mContent ) ) {
 +                                      $this->mSize = $this->mContent->getSize();
 +                              } else {
 +                                      #NOTE: this should never happen if we have either text or content object!
 +                                      $this->mSize = null;
 +                              }
                        }
 -                      # Same for sha1
 +
 +                      // Same for sha1
                        if ( $this->mSha1 === null ) {
                                $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
                        }
 +
 +                      // force lazy init
 +                      $this->getContentModel();
 +                      $this->getContentFormat();
                } else {
                        throw new MWException( 'Revision constructor passed invalid row format.' );
                }
                if( isset( $this->mTitle ) ) {
                        return $this->mTitle;
                }
 -              if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL
 +              if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
                        $dbr = wfGetDB( DB_SLAVE );
                        $row = $dbr->selectRow(
                                array( 'page', 'revision' ),
                                $this->mTitle = Title::newFromRow( $row );
                        }
                }
 +
 +              if ( !$this->mTitle && !is_null( $this->mPage ) && $this->mPage > 0 ) {
 +                      $this->mTitle = Title::newFromID( $this->mPage );
 +              }
 +
                return $this->mTitle;
        }
  
         *      Revision::RAW              get the text regardless of permissions
         * @param $user User object to check for, only if FOR_THIS_USER is passed
         *              to the $audience parameter
 +       *
 +       * @deprecated in 1.WD, use getContent() instead
 +       * @todo: replace usage in core
         * @return String
         */
        public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              $content = $this->getContent( $audience, $user );
 +              return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
 +      }
 +
 +      /**
 +       * Fetch revision content if it's available to the specified audience.
 +       * If the specified audience does not have the ability to view this
 +       * revision, null will be returned.
 +       *
 +       * @param $audience Integer: one of:
 +       *      Revision::FOR_PUBLIC       to be displayed to all users
 +       *      Revision::FOR_THIS_USER    to be displayed to $wgUser
 +       *      Revision::RAW              get the text regardless of permissions
 +       * @param $user User object to check for, only if FOR_THIS_USER is passed
 +       *              to the $audience parameter
 +       * @since 1.WD
 +       * @return Content
 +       */
 +      public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
                if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
 -                      return '';
 +                      return null;
                } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
 -                      return '';
 +                      return null;
                } else {
 -                      return $this->getRawText();
 +                      return $this->getContentInternal();
                }
        }
  
         * Fetch revision text without regard for view restrictions
         *
         * @return String
 +       *
 +       * @deprecated since 1.WD. Instead, use Revision::getContent( Revision::RAW )
 +       *                         or Revision::getSerializedData() as appropriate.
         */
        public function getRawText() {
 -              if( is_null( $this->mText ) ) {
 -                      // Revision text is immutable. Load on demand:
 -                      $this->mText = $this->loadText();
 -              }
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              return $this->getText( self::RAW );
 +      }
 +
 +      /**
 +       * Fetch original serialized data without regard for view restrictions
 +       *
 +       * @since 1.WD
 +       * @return String
 +       */
 +      public function getSerializedData() {
                return $this->mText;
        }
  
 +      /**
 +       * Gets the content object for the revision
 +       *
 +       * @since 1.WD
 +       * @return Content
 +       */
 +      protected function getContentInternal() {
 +              if( is_null( $this->mContent ) ) {
 +                      // Revision is immutable. Load on demand:
 +
 +                      $handler = $this->getContentHandler();
 +                      $format = $this->getContentFormat();
 +                      $title = $this->getTitle();
 +
 +                      if( is_null( $this->mText ) ) {
 +                              // Load text on demand:
 +                              $this->mText = $this->loadText();
 +                      }
 +
 +                      $this->mContent = is_null( $this->mText ) ? null : $handler->unserializeContent( $this->mText, $format );
 +              }
 +
 +              return $this->mContent->copy(); // NOTE: copy() will return $this for immutable content objects
 +      }
 +
 +      /**
 +       * Returns the content model for this revision.
 +       *
 +       * If no content model was stored in the database, $this->getTitle()->getContentModel() is
 +       * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
 +       * is used as a last resort.
 +       *
 +       * @return String the content model id associated with this revision, see the CONTENT_MODEL_XXX constants.
 +       **/
 +      public function getContentModel() {
 +              if ( !$this->mContentModel ) {
 +                      $title = $this->getTitle();
 +                      $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT );
 +
 +                      assert( !empty( $this->mContentModel ) );
 +              }
 +
 +              return $this->mContentModel;
 +      }
 +
 +      /**
 +       * Returns the content format for this revision.
 +       *
 +       * If no content format was stored in the database, the default format for this
 +       * revision's content model is returned.
 +       *
 +       * @return String the content format id associated with this revision, see the CONTENT_FORMAT_XXX constants.
 +       **/
 +      public function getContentFormat() {
 +              if ( !$this->mContentFormat ) {
 +                      $handler = $this->getContentHandler();
 +                      $this->mContentFormat = $handler->getDefaultFormat();
 +
 +                      assert( !empty( $this->mContentFormat ) );
 +              }
 +
 +              return $this->mContentFormat;
 +      }
 +
 +      /**
 +       * Returns the content handler appropriate for this revision's content model.
 +       *
 +       * @return ContentHandler
 +       */
 +      public function getContentHandler() {
 +              if ( !$this->mContentHandler ) {
 +                      $model = $this->getContentModel();
 +                      $this->mContentHandler = ContentHandler::getForModelID( $model );
 +
 +                      $format = $this->getContentFormat();
 +
 +                      if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
 +                              throw new MWException( "Oops, the content format $format is not supported for this content model, $model" );
 +                      }
 +              }
 +
 +              return $this->mContentHandler;
 +      }
 +
        /**
         * @return String
         */
         * @return Integer
         */
        public function insertOn( $dbw ) {
 -              global $wgDefaultExternalStore;
 +              global $wgDefaultExternalStore, $wgContentHandlerUseDB;
  
                wfProfileIn( __METHOD__ );
  
 +              $this->checkContentModel();
 +
                $data = $this->mText;
                $flags = self::compressRevisionText( $data );
  
                $rev_id = isset( $this->mId )
                        ? $this->mId
                        : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
 -              $dbw->insert( 'revision',
 -                      array(
 -                              'rev_id'         => $rev_id,
 -                              'rev_page'       => $this->mPage,
 -                              'rev_text_id'    => $this->mTextId,
 -                              'rev_comment'    => $this->mComment,
 -                              'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
 -                              'rev_user'       => $this->mUser,
 -                              'rev_user_text'  => $this->mUserText,
 -                              'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
 -                              'rev_deleted'    => $this->mDeleted,
 -                              'rev_len'        => $this->mSize,
 -                              'rev_parent_id'  => is_null( $this->mParentId )
 -                                      ? $this->getPreviousRevisionId( $dbw )
 -                                      : $this->mParentId,
 -                              'rev_sha1'       => is_null( $this->mSha1 )
 -                                      ? self::base36Sha1( $this->mText )
 -                                      : $this->mSha1
 -                      ), __METHOD__
 +              $row = array(
 +                      'rev_id'         => $rev_id,
 +                      'rev_page'       => $this->mPage,
 +                      'rev_text_id'    => $this->mTextId,
 +                      'rev_comment'    => $this->mComment,
 +                      'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
 +                      'rev_user'       => $this->mUser,
 +                      'rev_user_text'  => $this->mUserText,
 +                      'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
 +                      'rev_deleted'    => $this->mDeleted,
 +                      'rev_len'        => $this->mSize,
 +                      'rev_parent_id'  => is_null( $this->mParentId )
 +                              ? $this->getPreviousRevisionId( $dbw )
 +                              : $this->mParentId,
 +                      'rev_sha1'       => is_null( $this->mSha1 )
 +                              ? Revision::base36Sha1( $this->mText )
 +                              : $this->mSha1,
                );
  
-                       $defaultModel = ContentHandler::getDefaultModelFor( $this->getTitle() );
 +              if ( $wgContentHandlerUseDB ) {
 +                      //NOTE: Store null for the default model and format, to save space.
 +                      //XXX: Makes the DB sensitive to changed defaults. Make this behaviour optional? Only in miser mode?
 +
 +                      $model = $this->getContentModel();
 +                      $format = $this->getContentFormat();
 +
++                      $title = $this->getTitle();
++
++                      if ( $title === null ) {
++                              throw new MWException( "Insufficient information to determine the title of the revision's page!" );
++                      }
++
++                      $defaultModel = ContentHandler::getDefaultModelFor( $title );
 +                      $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
 +
 +                      $row[ 'rev_content_model' ] = ( $model === $defaultModel ) ? null : $model;
 +                      $row[ 'rev_content_format' ] = ( $format === $defaultFormat ) ? null : $format;
 +              }
 +
 +              $dbw->insert( 'revision', $row, __METHOD__ );
 +
                $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
  
                wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
                return $this->mId;
        }
  
 +      protected function checkContentModel() {
 +              global $wgContentHandlerUseDB;
 +
 +              $title = $this->getTitle(); //note: may return null for revisions that have not yet been inserted.
 +
 +              $model = $this->getContentModel();
 +              $format = $this->getContentFormat();
 +              $handler = $this->getContentHandler();
 +
 +              if ( !$handler->isSupportedFormat( $format ) ) {
 +                      $t = $title->getPrefixedDBkey();
 +
 +                      throw new MWException( "Can't use format $format with content model $model on $t" );
 +              }
 +
 +              if ( !$wgContentHandlerUseDB && $title ) {
 +                      // if $wgContentHandlerUseDB is not set, all revisions must use the default content model and format.
 +
 +                      $defaultModel = ContentHandler::getDefaultModelFor( $title );
 +                      $defaultHandler = ContentHandler::getForModelID( $defaultModel );
 +                      $defaultFormat = $defaultHandler->getDefaultFormat();
 +
 +                      if ( $this->getContentModel() != $defaultModel ) {
 +                              $t = $title->getPrefixedDBkey();
 +
 +                              throw new MWException( "Can't save non-default content model with \$wgContentHandlerUseDB disabled: "
 +                                                                              . "model is $model , default for $t is $defaultModel" );
 +                      }
 +
 +                      if ( $this->getContentFormat() != $defaultFormat ) {
 +                              $t = $title->getPrefixedDBkey();
 +
 +                              throw new MWException( "Can't use non-default content format with \$wgContentHandlerUseDB disabled: "
 +                                                                              . "format is $format, default for $t is $defaultFormat" );
 +                      }
 +              }
 +
 +              $content = $this->getContent( Revision::RAW );
 +
 +              if ( !$content->isValid() ) {
 +                      $t = $title->getPrefixedDBkey();
 +
 +                      throw new MWException( "Content of $t is not valid! Content model is $model" );
 +              }
 +      }
 +
        /**
         * Get the base 36 SHA-1 value for a string of text
         * @param $text String
         * @return Revision|null on error
         */
        public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
 +              global $wgContentHandlerUseDB;
 +
                wfProfileIn( __METHOD__ );
  
 +              $fields = array( 'page_latest', 'page_namespace', 'page_title',
 +                                              'rev_text_id', 'rev_len', 'rev_sha1' );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'rev_content_model';
 +                      $fields[] = 'rev_content_format';
 +              }
 +
                $current = $dbw->selectRow(
                        array( 'page', 'revision' ),
 -                      array( 'page_latest', 'page_namespace', 'page_title',
 -                              'rev_text_id', 'rev_len', 'rev_sha1' ),
 +                      $fields,
                        array(
                                'page_id' => $pageId,
                                'page_latest=rev_id',
                        __METHOD__ );
  
                if( $current ) {
 -                      $revision = new Revision( array(
 +                      $row = array(
                                'page'       => $pageId,
                                'comment'    => $summary,
                                'minor_edit' => $minor,
                                'parent_id'  => $current->page_latest,
                                'len'        => $current->rev_len,
                                'sha1'       => $current->rev_sha1
 -                              ) );
 +                      );
 +
 +                      if ( $wgContentHandlerUseDB ) {
 +                              $row[ 'content_model' ] = $current->rev_content_model;
 +                              $row[ 'content_format' ] = $current->rev_content_format;
 +                      }
 +
 +                      $revision = new Revision( $row );
                        $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) );
                } else {
                        $revision = null;
                }
                return 0;
        }
 -}
+       /**
+        * Check if no edits were made by other users since
+        * the time a user started editing the page. Limit to
+        * 50 revisions for the sake of performance.
+        *
+        * @since 1.20
+        *
+        * @param DatabaseBase|int $db the Database to perform the check on. May be given as a Database object or
+        *        a database identifier usable with wfGetDB.
+        * @param int $pageId the ID of the page in question
+        * @param int $userId the ID of the user in question
+        * @param string $since look at edits since this time
+        *
+        * @return bool True if the given user was the only one to edit since the given timestamp
+        */
+       public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
+               if ( !$userId ) return false;
+               if ( is_int( $db ) ) {
+                       $db = wfGetDB( $db );
+               }
+               $res = $db->select( 'revision',
+                       'rev_user',
+                       array(
+                               'rev_page' => $pageId,
+                               'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
+                       ),
+                       __METHOD__,
+                       array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ) );
+               foreach ( $res as $row ) {
+                       if ( $row->rev_user != $userId ) {
+                               return false;
+                       }
+               }
+               return true;
+       }
 +}
diff --combined includes/Title.php
@@@ -65,7 -65,6 +65,7 @@@ class Title 
        var $mFragment;                   // /< Title fragment (i.e. the bit after the #)
        var $mArticleID = -1;             // /< Article ID, fetched from the link cache on demand
        var $mLatestID = false;           // /< ID of most recent revision
 +      var $mContentModel = false;       // /< ID of the page's content model, i.e. one of the CONTENT_MODEL_XXX constants
        private $mEstimateRevisions;      // /< Estimated number of revisions; null of not loaded
        var $mRestrictions = array();     // /< Array of groups allowed to edit this article
        var $mOldRestrictions = false;
                }
        }
  
 +      /**
 +       * Returns a list of fields that are to be selected for initializing Title objects or LinkCache entries.
 +       * Uses $wgContentHandlerUseDB to determine whether to include page_content_model.
 +       *
 +       * @return array
 +       */
 +      protected static function getSelectFields() {
 +              global $wgContentHandlerUseDB;
 +
 +              $fields = array(
 +                      'page_namespace', 'page_title', 'page_id',
 +                      'page_len', 'page_is_redirect', 'page_latest',
 +              );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'page_content_model';
 +              }
 +
 +              return $fields;
 +      }
 +
        /**
         * Create a new Title from an article ID
         *
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                $row = $db->selectRow(
                        'page',
 -                      array(
 -                              'page_namespace', 'page_title', 'page_id',
 -                              'page_len', 'page_is_redirect', 'page_latest',
 -                      ),
 +                      self::getSelectFields(),
                        array( 'page_id' => $id ),
                        __METHOD__
                );
  
                $res = $dbr->select(
                        'page',
 -                      array(
 -                              'page_namespace', 'page_title', 'page_id',
 -                              'page_len', 'page_is_redirect', 'page_latest',
 -                      ),
 +                      self::getSelectFields(),
                        array( 'page_id' => $ids ),
                        __METHOD__
                );
                                $this->mRedirect = (bool)$row->page_is_redirect;
                        if ( isset( $row->page_latest ) )
                                $this->mLatestID = (int)$row->page_latest;
 +                      if ( isset( $row->page_content_model ) )
 +                              $this->mContentModel = strval( $row->page_content_model );
 +                      else
 +                              $this->mContentModel = false; # initialized lazily in getContentModel()
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
 +                      $this->mContentModel = false; # initialized lazily in getContentModel()
                }
        }
  
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = str_replace( '_', ' ', $title );
 +              $t->mContentModel = false; # initialized lazily in getContentModel()
                return $t;
        }
  
         *
         * @param $text String: Text with possible redirect
         * @return Title: The corresponding Title
 +       * @deprecated since 1.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 isConversionTable() {
++              //@todo: ConversionTable should become a separate content model.
++
                return $this->getNamespace() == NS_MEDIAWIKI &&
                        strpos( $this->getText(), 'Conversiontable/' ) === 0;
        }
         * @return Bool
         */
        public function isWikitextPage() {
 -              $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
 -              wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
 -              return $retval;
 +              return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
        }
  
        /**
 -       * Could this page contain custom CSS or JavaScript, based
 -       * on the title?
 +       * Could this page contain custom CSS or JavaScript for the global UI.
 +       * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
 +       * or CONTENT_MODEL_JAVASCRIPT.
 +       *
 +       * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage() for that!
 +       *
 +       * Note that this method should not return true for pages that contain and show "inactive" CSS or JS.
         *
         * @return Bool
         */
        public function isCssOrJsPage() {
 -              $retval = $this->mNamespace == NS_MEDIAWIKI
 -                      && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
 -              wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
 -              return $retval;
 +              $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
 +                      && ( $this->hasContentModel( CONTENT_MODEL_CSS )
 +                              || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
 +
 +              #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure
 +              #      hook funktions can force this method to return true even outside the mediawiki namespace.
 +
 +              wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) );
 +
 +              return $isCssOrJsPage;
        }
  
        /**
         * @return Bool
         */
        public function isCssJsSubpage() {
 -              return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
 +              return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +                              && ( $this->hasContentModel( CONTENT_MODEL_CSS )
 +                                      || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
        }
  
        /**
         * @return Bool
         */
        public function isCssSubpage() {
 -              return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
 +              return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +                      && $this->hasContentModel( CONTENT_MODEL_CSS ) );
        }
  
        /**
         * @return Bool
         */
        public function isJsSubpage() {
 -              return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
 +              return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +                      && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
        }
  
        /**
                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;
        }
  
        /**
                if ( $this->isSpecialPage() ) {
                        // special pages are in the user language
                        return $wgLang;
 -              } elseif ( $this->isCssOrJsPage() || $this->isCssJsSubpage() ) {
 -                      // css/js should always be LTR and is, in fact, English
 -                      return wfGetLangObj( 'en' );
 -              } elseif ( $this->getNamespace() == NS_MEDIAWIKI ) {
 -                      // Parse mediawiki messages with correct target language
 -                      list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $this->getText() );
 -                      return wfGetLangObj( $lang );
                }
 -              global $wgContLang;
 -              // If nothing special, it should be in the wiki content language
 -              $pageLang = $wgContLang;
 +
 +              //TODO: use the LinkCache to cache this! Note that this may depend on user settings, so the cache should be only per-request.
 +              //NOTE: ContentHandler::getPageLanguage() may need to load the content to determine the page language!
 +              $contentHandler = ContentHandler::getForTitle( $this );
 +              $pageLang = $contentHandler->getPageLanguage( $this );
 +
                // Hook at the end because we don't want to override the above stuff
                wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) );
                return wfGetLangObj( $pageLang );
         * @return Language
         */
        public function getPageViewLanguage() {
 -              $pageLang = $this->getPageLanguage();
 -              // If this is nothing special (so the content is converted when viewed)
 -              if ( !$this->isSpecialPage()
 -                      && !$this->isCssOrJsPage() && !$this->isCssJsSubpage()
 -                      && $this->getNamespace() !== NS_MEDIAWIKI
 -              ) {
 +              global $wgLang;
 +
 +              if ( $this->isSpecialPage() ) {
                        // If the user chooses a variant, the content is actually
                        // in a language whose code is the variant code.
 -                      $variant = $pageLang->getPreferredVariant();
 -                      if ( $pageLang->getCode() !== $variant ) {
 -                              $pageLang = Language::factory( $variant );
 +                      $variant = $wgLang->getPreferredVariant();
 +                      if ( $wgLang->getCode() !== $variant ) {
 +                              return Language::factory( $variant );
                        }
 +
 +                      return $wgLang;
                }
 +
 +              //NOTE: can't be cached persistently, depends on user settings
 +              //NOTE: ContentHandler::getPageViewLanguage() may need to load the content to determine the page language!
 +              $contentHandler = ContentHandler::getForTitle( $this );
 +              $pageLang = $contentHandler->getPageViewLanguage( $this );
                return $pageLang;
        }
  }
diff --combined includes/WikiPage.php
@@@ -187,21 -187,7 +187,21 @@@ class WikiPage extends Page implements 
         * @return Array
         */
        public function getActionOverrides() {
 -              return array();
 +              $content_handler = $this->getContentHandler();
 +              return $content_handler->getActionOverrides();
 +      }
 +
 +      /**
 +       * Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
 +       *
 +       * Shorthand for ContentHandler::getForModelID( $this->getContentModel() );
 +       *
 +       * @return ContentHandler
 +       *
 +       * @since 1.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.
         */
        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
  
                wfProfileIn( __METHOD__ );
  
 +              if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) {
 +                      wfProfileOut( __METHOD__ );
 +                      return Status::newFatal( 'content-not-allowed-here',
 +                              ContentHandler::getLocalizedName( $content->getModel() ),
 +                              $this->getTitle()->getPrefixedText() );
 +              }
 +
                $user = is_null( $user ) ? $wgUser : $user;
                $status = Status::newGood( array() );
  
  
                $flags = $this->checkFlags( $flags );
  
 -              if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
 -                      $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) )
 -              {
 -                      wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
 +              # handle hook
 +              $hook_args = array( &$this, &$user, &$content, &$summary,
 +                                                      $flags & EDIT_MINOR, null, null, &$flags, &$status );
 +
 +              if ( !wfRunHooks( 'ArticleContentSave', $hook_args )
 +                      || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) {
 +
 +                      wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" );
  
                        if ( $status->isOK() ) {
                                $status->fatal( 'edit-hook-aborted' );
                $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' );
                $bot = $flags & EDIT_FORCE_BOT;
  
 -              $oldtext = $this->getRawText(); // current revision
 -              $oldsize = strlen( $oldtext );
 +              $old_content = $this->getContent( Revision::RAW ); // current revision's content
 +
 +              $oldsize = $old_content ? $old_content->getSize() : 0;
                $oldid = $this->getLatest();
                $oldIsRedirect = $this->isRedirect();
                $oldcountable = $this->isCountable();
  
 +              $handler = $content->getContentHandler();
 +
                # Provide autosummaries if one is not provided and autosummaries are enabled.
                if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
 -                      $summary = self::getAutosummary( $oldtext, $text, $flags );
 +                      if ( !$old_content ) $old_content = null;
 +                      $summary = $handler->getAutosummary( $old_content, $content, $flags );
                }
  
 -              $editInfo = $this->prepareTextForEdit( $text, null, $user );
 -              $text = $editInfo->pst;
 -              $newsize = strlen( $text );
 +              $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format );
 +              $serialized = $editInfo->pst;
 +              $content = $editInfo->pstContent;
 +              $newsize =  $content->getSize();
  
                $dbw = wfGetDB( DB_MASTER );
                $now = wfTimestampNow();
  
                                wfProfileOut( __METHOD__ );
                                return $status;
 -                      } elseif ( $oldtext === false ) {
 +                      } elseif ( !$old_content ) {
                                # Sanity check for bug 37225
                                wfProfileOut( __METHOD__ );
                                throw new MWException( "Could not find text for current revision {$oldid}." );
                                'page'       => $this->getId(),
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
 -                              'text'       => $text,
 +                              'text'       => $serialized,
 +                              'len'        => $newsize,
                                'parent_id'  => $oldid,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
 -                              'timestamp'  => $now
 -                      ) );
 -                      # Bug 37225: use accessor to get the text as Revision may trim it.
 -                      # After trimming, the text may be a duplicate of the current text.
 -                      $text = $revision->getText(); // sanity; EditPage should trim already
 +                              'timestamp'  => $now,
 +                              'content_model' => $content->getModel(),
 +                              'content_format' => $serialisation_format,
 +                      ) ); #XXX: pass content object?!
  
 -                      $changed = ( strcmp( $text, $oldtext ) != 0 );
 +                      $changed = !$content->equals( $old_content );
  
                        if ( $changed ) {
 +                              if ( !$content->isValid() ) {
 +                                      throw new MWException( "New content failed validity check!" );
 +                              }
 +
                                $dbw->begin( __METHOD__ );
 +
 +                              $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
 +                              $status->merge( $prepStatus );
 +
 +                              if ( !$status->isOK() ) {
 +                                      $dbw->rollback();
 +
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
 +
                                $revisionId = $revision->insertOn( $dbw );
  
                                # Update page
                        }
  
                        # Update links tables, site stats, etc.
 -                      $this->doEditUpdates( $revision, $user, array( 'changed' => $changed,
 -                              'oldcountable' => $oldcountable ) );
 +                      $this->doEditUpdates(
 +                              $revision,
 +                              $user,
 +                              array(
 +                                      'changed' => $changed,
 +                                      'oldcountable' => $oldcountable
 +                              )
 +                      );
  
                        if ( !$changed ) {
                                $status->warning( 'edit-no-change' );
  
                        $dbw->begin( __METHOD__ );
  
 +                      $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
 +                      $status->merge( $prepStatus );
 +
 +                      if ( !$status->isOK() ) {
 +                              $dbw->rollback();
 +
 +                              wfProfileOut( __METHOD__ );
 +                              return $status;
 +                      }
 +
 +                      $status->merge( $prepStatus );
 +
                        # Add the page record; stake our claim on this title!
                        # This will return false if the article already exists
                        $newid = $this->insertOn( $dbw );
                                'page'       => $newid,
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
 -                              'text'       => $text,
 +                              'text'       => $serialized,
 +                              'len'        => $newsize,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
 -                              'timestamp'  => $now
 +                              'timestamp'  => $now,
 +                              'content_model' => $content->getModel(),
 +                              'content_format' => $serialisation_format,
                        ) );
                        $revisionId = $revision->insertOn( $dbw );
  
                        # Bug 37225: use accessor to get the text as Revision may trim it
 -                      $text = $revision->getText(); // sanity; EditPage should trim already
 +                      $content = $revision->getContent(); // sanity; get normalized version
  
                        # Update the page record with revision data
                        $this->updateRevisionOn( $dbw, $revision, 0 );
                                        $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
                                # Add RC row to the DB
                                $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
 -                                      '', strlen( $text ), $revisionId, $patrolled );
 +                                      '', $content->getSize(), $revisionId, $patrolled );
  
                                # Log auto-patrolled edits
                                if ( $patrolled ) {
                        # Update links, etc.
                        $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
  
 -                      wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
 -                              $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
 +                      $hook_args = array( &$this, &$user, $content, $summary,
 +                                                              $flags & EDIT_MINOR, null, null, &$flags, $revision );
 +
 +                      ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args );
 +                      wfRunHooks( 'ArticleContentInsertComplete', $hook_args );
                }
  
                # Do updates right now unless deferral was requested
                // Return the new revision (or null) to the caller
                $status->value['revision'] = $revision;
  
 -              wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
 -                      $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
 +              $hook_args = array( &$this, &$user, $content, $summary,
 +                                                      $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId );
 +
 +              ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args );
 +              wfRunHooks( 'ArticleContentSaveComplete', $hook_args );
  
                # Promote user to any groups they meet the criteria for
                $user->addAutopromoteOnceGroups( 'onEdit' );
  
        /**
         * Get parser options suitable for rendering the primary article wikitext
-        * @param User|string $user User object or 'canonical'
+        *
+        * @param IContextSource|User|string $context One of the following:
+        *        - IContextSource: Use the User and the Language of the provided
+        *          context
+        *        - User: Use the provided User object and $wgLang for the language,
+        *          so use an IContextSource object if possible.
+        *        - 'canonical': Canonical options (anonymous user with default
+        *          preferences and content language).
         * @return ParserOptions
         */
-       public function makeParserOptions( $user ) {
+       public function makeParserOptions( $context ) {
                global $wgContLang;
-               if ( $user instanceof User ) { // settings per user (even anons)
-                       $options = ParserOptions::newFromUser( $user );
+               if ( $context instanceof IContextSource ) {
+                       $options = ParserOptions::newFromContext( $context );
+               } elseif ( $context instanceof User ) { // settings per user (even anons)
+                       $options = ParserOptions::newFromUser( $context );
                } else { // canonical settings
                        $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
                }
+               if ( $this->getTitle()->isConversionTable() ) {
++                      //@todo: ConversionTable should become a separate content model.
+                       $options->disableContentConversion();
+               }
                $options->enableLimitReport(); // show inclusion/loop reports
                $options->setTidy( true ); // fix bad HTML
                return $options;
        }
  
        /**
         * Prepare text which is about to be saved.
         * Returns a stdclass with source, pst and output members
 -       * @return bool|object
 +       *
 +       * @deprecated in 1.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???
 +              //XXX: check $user->getId() here???
 +
                if ( $this->mPreparedEdit
 -                      && $this->mPreparedEdit->newText == $text
 +                      && $this->mPreparedEdit->newContent
 +                      && $this->mPreparedEdit->newContent->equals( $content )
                        && $this->mPreparedEdit->revid == $revid
 +                      && $this->mPreparedEdit->format == $serialization_format
 +                      #XXX: also check $user here?
                ) {
                        // Already prepared
                        return $this->mPreparedEdit;
  
                $edit = (object)array();
                $edit->revid = $revid;
 -              $edit->newText = $text;
 -              $edit->pst = $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
 +
 +              $edit->pstContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
 +              $edit->pst = $edit->pstContent->serialize( $serialization_format ); #XXX: do we need this??
 +              $edit->format = $serialization_format;
 +
                $edit->popts = $this->makeParserOptions( 'canonical' );
 -              $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid );
 -              $edit->oldText = $this->getRawText();
 +
 +              $edit->output = $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts );
 +
 +              $edit->newContent = $content;
 +              $edit->oldContent = $this->getContent( Revision::RAW );
 +
 +              #NOTE: B/C for hooks! don't use these fields!
 +              $edit->newText = ContentHandler::getContentText( $edit->newContent );
 +              $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : '';
  
                $this->mPreparedEdit = $edit;
  
         * Purges pages that include this page if the text was changed here.
         * Every 100th edit, prune the recent changes table.
         *
 -       * @private
         * @param $revision Revision object
         * @param $user User object that did the revision
         * @param $options Array of options, following indexes are used:
                wfProfileIn( __METHOD__ );
  
                $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null );
 -              $text = $revision->getText();
 +              $content = $revision->getContent();
  
                # Parse the text
                # Be careful not to double-PST: $text is usually already PST-ed once
                if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
                        wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
 -                      $editInfo = $this->prepareTextForEdit( $text, $revision->getId(), $user );
 +                      $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user );
                } else {
                        wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
                        $editInfo = $this->mPreparedEdit;
                }
  
                # Update the links tables and other secondary data
 -              $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle );
 +              $updates = $content->getSecondaryDataUpdates( $this->getTitle(), null, true, $editInfo->output );
                DataUpdate::runUpdates( $updates );
  
                wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
                }
  
                DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) );
 -              DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) );
 +              DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content->getTextForSearchIndex() ) );
 +              #@TODO: let the search engine decide what to do with the content object
  
                # If this is another user's talk page, update newtalk.
                # Don't do this if $options['changed'] = false (null-edits) nor if
                }
  
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
 -                      MessageCache::singleton()->replace( $shortTitle, $text );
 +                      #XXX: could skip pseudo-messages like js/css here, based on content model.
 +                      $msgtext = $content->getWikitextForTransclusion();
 +                      if ( $msgtext === false || $msgtext === null ) $msgtext = '';
 +
 +                      MessageCache::singleton()->replace( $shortTitle, $msgtext );
                }
  
                if( $options['created'] ) {
         * @param $user User The relevant user
         * @param $comment String: comment submitted
         * @param $minor Boolean: whereas it's a minor modification
 +       *
 +       * @deprecated since 1.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
                }
  
                # 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.
 +              # NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't.
  
 -              # 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();
 -              }
 +              wfDeprecated( __METHOD__, '1.WD' );
  
 -              # 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
 +              $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
 +              $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
 +              $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
  
 -                      $truncatedtext = $wgContLang->truncate(
 -                              $newtext,
 -                              max( 0, 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ) );
 -
 -                      return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
 -                              ->inContentLanguage()->text();
 -              }
 -
 -              # If we reach this point, there's no applicable autosummary for our case, so our
 -              # autosummary is empty.
 -              return '';
 +              return $handler->getAutosummary( $oldContent, $newContent, $flags );
        }
  
        /**
         *    if no revision occurred
         */
        public function getAutoDeleteReason( &$hasHistory ) {
 -              global $wgContLang;
 -
 -              // Get the last revision
 -              $rev = $this->getRevision();
 -
 -              if ( is_null( $rev ) ) {
 -                      return false;
 -              }
 -
 -              // Get the article's contents
 -              $contents = $rev->getText();
 -              $blank = false;
 -
 -              // If the page is blank, use the text from the previous revision,
 -              // which can only be blank if there's a move/import/protect dummy revision involved
 -              if ( $contents == '' ) {
 -                      $prev = $rev->getPrevious();
 -
 -                      if ( $prev )    {
 -                              $contents = $prev->getText();
 -                              $blank = true;
 -                      }
 -              }
 -
 -              $dbw = wfGetDB( DB_MASTER );
 -
 -              // Find out if there was only one contributor
 -              // Only scan the last 20 revisions
 -              $res = $dbw->select( 'revision', 'rev_user_text',
 -                      array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
 -                      __METHOD__,
 -                      array( 'LIMIT' => 20 )
 -              );
 -
 -              if ( $res === false ) {
 -                      // This page has no revisions, which is very weird
 -                      return false;
 -              }
 -
 -              $hasHistory = ( $res->numRows() > 1 );
 -              $row = $dbw->fetchObject( $res );
 -
 -              if ( $row ) { // $row is false if the only contributor is hidden
 -                      $onlyAuthor = $row->rev_user_text;
 -                      // Try to find a second contributor
 -                      foreach ( $res as $row ) {
 -                              if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
 -                                      $onlyAuthor = false;
 -                                      break;
 -                              }
 -                      }
 -              } else {
 -                      $onlyAuthor = false;
 -              }
 -
 -              // Generate the summary with a '$1' placeholder
 -              if ( $blank ) {
 -                      // The current revision is blank and the one before is also
 -                      // blank. It's just not our lucky day
 -                      $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
 -              } else {
 -                      if ( $onlyAuthor ) {
 -                              $reason = wfMessage(
 -                                      'excontentauthor',
 -                                      '$1',
 -                                      $onlyAuthor
 -                              )->inContentLanguage()->text();
 -                      } else {
 -                              $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
 -                      }
 -              }
 -
 -              if ( $reason == '-' ) {
 -                      // Allow these UI messages to be blanked out cleanly
 -                      return '';
 -              }
 -
 -              // Replace newlines with spaces to prevent uglyness
 -              $contents = preg_replace( "/[\n\r]/", ' ', $contents );
 -              // Calculate the maximum amount of chars to get
 -              // Max content length = max comment length - length of the comment (excl. $1)
 -              $maxLength = 255 - ( strlen( $reason ) - 2 );
 -              $contents = $wgContLang->truncate( $contents, $maxLength );
 -              // Remove possible unfinished links
 -              $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
 -              // Now replace the '$1' placeholder
 -              $reason = str_replace( '$1', $contents, $reason );
 -
 -              return $reason;
 +              return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
        }
  
        /**
                global $wgUser;
                return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid );
        }
 +
 +      /**
 +       * Returns a list of updates to be performed when this page is deleted. The updates should remove any information
 +       * about this page from secondary data stores such as links tables.
 +       *
 +       * @param Content|null $content optional Content object for determining the necessary updates
 +       * @return Array an array of DataUpdates objects
 +       */
 +      public function getDeletionUpdates( Content $content = null ) {
 +              if ( !$content ) {
 +                      // load content object, which may be used to determine the necessary updates
 +                      // XXX: the content may not be needed to determine the updates, then this would be overhead.
 +                      $content = $this->getContent( Revision::RAW );
 +              }
 +
 +              if ( !$content ) {
 +                      $updates = array();
 +              } else {
 +                      $updates = $content->getDeletionUpdates( $this );
 +              }
 +
 +              wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) );
 +              return $updates;
 +      }
 +
  }
  
  class PoolWorkArticleView extends PoolCounterWork {
        private $parserOptions;
  
        /**
 -       * @var string|null
 +       * @var Content|null
         */
 -      private $text;
 +      private $content = null;
  
        /**
         * @var ParserOutput|bool
         * @param $revid Integer: ID of the revision being parsed
         * @param $useParserCache Boolean: whether to use the parser cache
         * @param $parserOptions parserOptions to use for the parse operation
 -       * @param $text String: text to parse or null to load it
 +       * @param $content Content|String: content to parse or null to load it; may also be given as a wikitext string, for BC
         */
 -      function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) {
 +      function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null ) {
 +              if ( is_string($content) ) { #BC: old style call
 +                      $modelId = $page->getRevision()->getContentModel();
 +                      $format = $page->getRevision()->getContentFormat();
 +                      $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format );
 +              }
 +
                $this->page = $page;
                $this->revid = $revid;
                $this->cacheable = $useParserCache;
                $this->parserOptions = $parserOptions;
 -              $this->text = $text;
 +              $this->content = $content;
                $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions );
                parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid );
        }
         * @return bool
         */
        function doWork() {
 -              global $wgParser, $wgUseFileCache;
 +              global $wgUseFileCache;
 +
 +              // @todo: several of the methods called on $this->page are not declared in Page, but present
 +              //        in WikiPage and delegated by Article.
  
                $isCurrent = $this->revid === $this->page->getLatest();
  
 -              if ( $this->text !== null ) {
 -                      $text = $this->text;
 +              if ( $this->content !== null ) {
 +                      $content = $this->content;
                } elseif ( $isCurrent ) {
 -                      $text = $this->page->getRawText();
 +                      #XXX: why use RAW audience here, and PUBLIC (default) below?
 +                      $content = $this->page->getContent( Revision::RAW );
                } else {
                        $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid );
                        if ( $rev === null ) {
                                return false;
                        }
 -                      $text = $rev->getText();
 +
 +                      #XXX: why use PUBLIC audience here (default), and RAW above?
 +                      $content = $rev->getContent();
                }
  
                $time = - microtime( true );
 -              $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(),
 -                      $this->parserOptions, true, true, $this->revid );
 +              $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions );
                $time += microtime( true );
  
                # Timing hack
                return false;
        }
  }
 +
   * @ingroup API
   */
  class ApiParse extends ApiBase {
 -      private $section, $text, $pstText = null;
 +
 +      /** @var String $section */
 +      private $section = null;
 +
 +      /** @var Content $content */
 +      private $content = null;
 +
 +      /** @var Content $pstContent */
 +      private $pstContent = null;
  
        public function __construct( $main, $action ) {
                parent::__construct( $main, $action );
@@@ -52,9 -44,6 +52,9 @@@
                $pageid = $params['pageid'];
                $oldid = $params['oldid'];
  
 +              $model = $params['contentmodel'];
 +              $format = $params['contentformat'];
 +
                if ( !is_null( $page ) && ( !is_null( $text ) || $title != 'API' ) ) {
                        $this->dieUsage( 'The page parameter cannot be used together with the text and title parameters', 'params' );
                }
                        $this->getContext()->setLanguage( Language::factory( $params['uselang'] ) );
                }
  
-               $popts = ParserOptions::newFromContext( $this->getContext() );
-               $popts->setTidy( true );
-               $popts->enableLimitReport( !$params['disablepp'] );
                $redirValues = null;
  
                // Return result
                                }
  
                                $titleObj = $rev->getTitle();
                                $wgTitle = $titleObj;
+                               $pageObj = WikiPage::factory( $titleObj );
+                               $popts = $pageObj->makeParserOptions( $this->getContext() );
+                               $popts->enableLimitReport( !$params['disablepp'] );
  
                                // If for some reason the "oldid" is actually the current revision, it may be cached
-                               if ( $titleObj->getLatestRevID() === intval( $oldid ) )  {
+                               if ( $rev->isCurrent() )  {
                                        // May get from/save to parser cache
 -                                      $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid,
 -                                               isset( $prop['wikitext'] ) ) ;
 +                                      $pageObj = WikiPage::factory( $titleObj );
-                                       $p_result = $this->getParsedContent( $pageObj, $popts, $pageid, isset( $prop['wikitext'] ) ) ;
++                                      $p_result = $this->getParsedContent( $pageObj, $popts, $pageid, 
++                                              isset( $prop['wikitext'] ) ) ;
                                } else { // This is an old revision, so get the text differently
 -                                      $this->text = $rev->getText( Revision::FOR_THIS_USER, $this->getUser() );
 +                                      $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
  
                                        if ( $this->section !== false ) {
 -                                              $this->text = $this->getSectionText( $this->text, 'r' . $rev->getId() );
 +                                              $this->content = $this->getSectionContent( $this->content, 'r' . $rev->getId() );
                                        }
  
                                        // Should we save old revision parses to the parser cache?
 -                                      $p_result = $wgParser->parse( $this->text, $titleObj, $popts );
 +                                      $p_result = $this->content->getParserOutput( $titleObj, $popts );
                                }
                        } else { // Not $oldid, but $pageid or $page
                                if ( $params['redirects'] ) {
                                        foreach ( (array)$redirValues as $r ) {
                                                $to = $r['to'];
                                        }
-                                       $titleObj = Title::newFromText( $to );
-                               } else {
-                                       if ( !is_null ( $pageid ) ) {
-                                               $reqParams['pageids'] = $pageid;
-                                               $titleObj = Title::newFromID( $pageid );
-                                       } else { // $page
-                                               $to = $page;
-                                               $titleObj = Title::newFromText( $to );
-                                       }
-                               }
-                               if ( !is_null ( $pageid ) ) {
-                                       if ( !$titleObj ) {
-                                               // Still throw nosuchpageid error if pageid was provided
-                                               $this->dieUsageMsg( array( 'nosuchpageid', $pageid ) );
-                                       }
-                               } elseif ( !$titleObj || !$titleObj->exists() ) {
-                                       $this->dieUsage( "The page you specified doesn't exist", 'missingtitle' );
+                                       $pageParams = array( 'title' => $to );
+                               } elseif ( !is_null( $pageid ) ) {
+                                       $pageParams = array( 'pageid' => $pageid );
+                               } else { // $page
+                                       $pageParams = array( 'title' => $page );
                                }
+                               $pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' );
+                               $titleObj = $pageObj->getTitle();
                                $wgTitle = $titleObj;
  
                                if ( isset( $prop['revid'] ) ) {
-                                       $oldid = $titleObj->getLatestRevID();
+                                       $oldid = $pageObj->getLatest();
                                }
  
-                               $pageObj = WikiPage::factory( $titleObj );
++
+                               $popts = $pageObj->makeParserOptions( $this->getContext() );
+                               $popts->enableLimitReport( !$params['disablepp'] );
  
                                // Potentially cached
-                               $p_result = $this->getParsedContent( $pageObj, $popts, $pageid, isset( $prop['wikitext'] ) ) ;
 -                              $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid,
 -                                       isset( $prop['wikitext'] ) ) ;
++                              $p_result = $this->getParsedContent( $pageObj, $popts, $pageid, 
++                                      isset( $prop['wikitext'] ) ) ;
                        }
                } else { // Not $oldid, $pageid, $page. Hence based on $text
 -
 -                      if ( is_null( $text ) ) {
 -                              $this->dieUsage( 'The text parameter should be passed with the title parameter. Should you be using the "page" parameter instead?', 'params' );
 -                      }
 -                      $this->text = $text;
                        $titleObj = Title::newFromText( $title );
                        if ( !$titleObj ) {
                                $this->dieUsageMsg( array( 'invalidtitle', $title ) );
                        }
                        $wgTitle = $titleObj;
+                       $pageObj = WikiPage::factory( $titleObj );
+                       $popts = $pageObj->makeParserOptions( $this->getContext() );
+                       $popts->enableLimitReport( !$params['disablepp'] );
  
 +                      if ( is_null( $text ) ) {
 +                              $this->dieUsage( 'The text parameter should be passed with the title parameter. Should you be using the "page" parameter instead?', 'params' );
 +                      }
 +
 +                      try {
 +                              $this->content = ContentHandler::makeContent( $text, $titleObj, $model, $format );
 +                      } catch ( MWContentSerializationException $ex ) {
 +                              $this->dieUsage( $ex->getMessage(), 'parseerror' );
 +                      }
 +
                        if ( $this->section !== false ) {
 -                              $this->text = $this->getSectionText( $this->text, $titleObj->getText() );
 +                              $this->content = $this->getSectionContent( $this->content, $titleObj->getText() );
                        }
  
                        if ( $params['pst'] || $params['onlypst'] ) {
 -                              $this->pstText = $wgParser->preSaveTransform( $this->text, $titleObj, $this->getUser(), $popts );
 +                              $this->pstContent = $this->content->preSaveTransform( $titleObj, $this->getUser(), $popts );
                        }
                        if ( $params['onlypst'] ) {
                                // Build a result and bail out
                                $result_array = array();
                                $result_array['text'] = array();
 -                              $result->setContent( $result_array['text'], $this->pstText );
 +                              $result->setContent( $result_array['text'], $this->pstContent->serialize( $format ) );
                                if ( isset( $prop['wikitext'] ) ) {
                                        $result_array['wikitext'] = array();
 -                                      $result->setContent( $result_array['wikitext'], $this->text );
 +                                      $result->setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
                                }
                                $result->addValue( null, $this->getModuleName(), $result_array );
                                return;
                        }
 +
                        // Not cached (save or load)
 -                      $p_result = $wgParser->parse( $params['pst'] ? $this->pstText : $this->text, $titleObj, $popts );
 +                      if ( $params['pst'] ) {
 +                              $p_result = $this->pstContent->getParserOutput( $titleObj, $popts );
 +                      } else {
 +                              $p_result = $this->content->getParserOutput( $titleObj, $popts );
 +                      }
                }
  
                $result_array = array();
  
                if ( isset( $prop['wikitext'] ) ) {
                        $result_array['wikitext'] = array();
 -                      $result->setContent( $result_array['wikitext'], $this->text );
 -                      if ( !is_null( $this->pstText ) ) {
 +                      $result->setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
 +                      if ( !is_null( $this->pstContent ) ) {
                                $result_array['psttext'] = array();
 -                              $result->setContent( $result_array['psttext'], $this->pstText );
 +                              $result->setContent( $result_array['psttext'], $this->pstContent->serialize( $format ) );
                        }
                }
                if ( isset( $prop['properties'] ) ) {
                }
  
                if ( $params['generatexml'] ) {
 +                      if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
 +                              $this->dieUsage( "generatexml is only supported for wikitext content", "notwikitext" );
 +                      }
 +
                        $wgParser->startExternalParse( $titleObj, $popts, OT_PREPROCESS );
 -                      $dom = $wgParser->preprocessToDom( $this->text );
 +                      $dom = $wgParser->preprocessToDom( $this->content->getNativeData() );
                        if ( is_callable( array( $dom, 'saveXML' ) ) ) {
                                $xml = $dom->saveXML();
                        } else {
         * @param $getWikitext Bool
         * @return ParserOutput
         */
 -      private function getParsedSectionOrText( $page, $popts, $pageId = null, $getWikitext = false ) {
 -              global $wgParser;
 +      private function getParsedContent( WikiPage $page, $popts, $pageId = null, $getWikitext = false ) {
 +              $this->content = $page->getContent( Revision::RAW ); //XXX: really raw?
  
                if ( $this->section !== false ) {
 -                      $this->text = $this->getSectionText( $page->getRawText(), !is_null( $pageId )
 -                                      ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText() );
 +                      $this->content = $this->getSectionContent( $this->content, !is_null( $pageId )
 +                                                                                                              ? 'page id ' . $pageId : $page->getTitle()->getText() );
  
                        // Not cached (save or load)
 -                      return $wgParser->parse( $this->text, $page->getTitle(), $popts );
 +                      return $this->content->getParserOutput( $page->getTitle(), $popts );
                } else {
                        // Try the parser cache first
                        // getParserOutput will save to Parser cache if able
                                $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' );
                        }
                        if ( $getWikitext ) {
 -                              $this->text = $page->getRawText();
 +                              $this->content = $page->getContent( Revision::RAW );
                        }
                        return $pout;
                }
        }
  
 -      private function getSectionText( $text, $what ) {
 -              global $wgParser;
 +      private function getSectionContent( Content $content, $what ) {
                // Not cached (save or load)
 -              $text = $wgParser->getSection( $text, $this->section, false );
 -              if ( $text === false ) {
 +              $section = $content->getSection( $this->section );
 +              if ( $section === false ) {
                        $this->dieUsage( "There is no section {$this->section} in " . $what, 'nosuchsection' );
                }
 -              return $text;
 +              if ( $section === null ) {
 +                      $this->dieUsage( "Sections are not supported by " . $what, 'nosuchsection' );
 +                      $section = false;
 +              }
 +              return $section;
        }
  
        private function formatLangLinks( $links ) {
                        'section' => null,
                        'disablepp' => false,
                        'generatexml' => false,
 +                      'contentformat' => array(
 +                              ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
 +                      ),
 +                      'contentmodel' => array(
 +                              ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
 +                      )
                );
        }
  
                        'section' => 'Only retrieve the content of this section number',
                        'disablepp' => 'Disable the PP Report from the parser output',
                        'generatexml' => 'Generate XML parse tree',
 +                      'contentformat' => 'Content serialization format used for the input text',
 +                      'contentmodel' => 'Content model of the new content',
                );
        }
  
                        array( 'code' => 'nosuchsection', 'info' => 'There is no section sectionnumber in page' ),
                        array( 'nosuchpageid' ),
                        array( 'invalidtitle', 'title' ),
 +                      array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ),
 +                      array( 'code' => 'notwikitext', 'info' => 'The requested operation is only supported on wikitext content.' ),
                ) );
        }
  
@@@ -38,7 -38,7 +38,7 @@@ class DifferenceEngine extends ContextS
         * @private
         */
        var $mOldid, $mNewid;
 -      var $mOldtext, $mNewtext;
 +      var $mOldContent, $mNewContent;
        protected $mDiffLang;
  
        /**
                # we'll use the application/x-external-editor interface to call
                # an external diff tool like kompare, kdiff3, etc.
                if ( ExternalEdit::useExternalEngine( $this->getContext(), 'diff' ) ) {
 +                      //TODO: come up with a good solution for non-text content here.
 +                      //      at least, the content format needs to be passed to the client somehow.
 +                      //      Currently, action=raw will just fail for non-text content.
 +
                        $urls = array(
                                'File' => array( 'Extension' => 'wiki', 'URL' =>
                                        # This should be mOldPage, but it may not be set, see below.
                        $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
                        $out->setArticleFlag( true );
  
 +                      // NOTE: only needed for B/C: custom rendering of JS/CSS via hook
                        if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
                                // Stolen from Article::view --AG 2007-10-11
                                // Give hooks a chance to customise the output
                                // @TODO: standardize this crap into one function
 -                              if ( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
 -                                      // Wrap the whole lot in a <pre> and don't parse
 -                                      $m = array();
 -                                      preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m );
 -                                      $out->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
 -                                      $out->addHTML( htmlspecialchars( $this->mNewtext ) );
 -                                      $out->addHTML( "\n</pre>\n" );
 +                              if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
 +                                      // NOTE: deprecated hook, B/C only
 +                                      // use the content object's own rendering
 +                                      $po = $this->mContentObject->getParserOutput();
 +                                      $out->addHTML( $po->getText() );
                                }
 -                      } elseif ( !wfRunHooks( 'ArticleViewCustom', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
 +                      } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
 +                              // Handled by extension
 +                      } elseif( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
 +                              // NOTE: deprecated hook, B/C only
                                // Handled by extension
                        } else {
                                // Normal page
                                        $wikiPage = WikiPage::factory( $this->mNewPage );
                                }
  
 -                              $parserOptions = $wikiPage->makeParserOptions( $this->getContext() );
 -
 -                              if ( !$this->mNewRev->isCurrent() ) {
 -                                      $parserOptions->setEditSection( false );
 -                              }
 -
 -                              $parserOutput = $wikiPage->getParserOutput( $parserOptions, $this->mNewid );
 +                              $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
  
                                # WikiPage::getParserOutput() should not return false, but just in case
                                if( $parserOutput ) {
                wfProfileOut( __METHOD__ );
        }
  
-               $parserOptions = ParserOptions::newFromContext( $this->getContext() );
-               $parserOptions->enableLimitReport();
-               $parserOptions->setTidy( true );
 +      protected function getParserOutput( WikiPage $page, Revision $rev ) {
++              $parserOptions = $page->makeParserOptions( $this->getContext() );
 +
 +              if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( "edit" ) ) {
 +                      $parserOptions->setEditSection( false );
 +              }
 +
 +              $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
 +              return $parserOutput;
 +      }
 +
        /**
         * Get the diff text, send it to the OutputPage object
         * Returns false if the diff could not be generated, otherwise returns true
                        return false;
                }
  
 -              $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
 +              $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
  
                // Save to cache for 7 days
                if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
                }
        }
  
 +      /**
 +       * Generate a diff, no caching.
 +       *
 +       * Subclasses may override this to provide a
 +       *
 +       * @param $old Content: old content
 +       * @param $new Content: new content
 +       *
 +       * @since 1.WD
 +       */
 +      function generateContentDiffBody( Content $old, Content $new ) {
 +              if ( !( $old instanceof TextContent ) ) {
 +                      throw new MWException( "Diff not implemented for " . get_class( $old ) . "; "
 +                                                              . "override generateContentDiffBody to fix this." );
 +              }
 +
 +              if ( !( $new instanceof TextContent ) ) {
 +                      throw new MWException( "Diff not implemented for " . get_class( $new ) . "; "
 +                              . "override generateContentDiffBody to fix this." );
 +              }
 +
 +              $otext = $old->serialize();
 +              $ntext = $new->serialize();
 +
 +              return $this->generateTextDiffBody( $otext, $ntext );
 +      }
 +
        /**
         * Generate a diff, no caching
         *
         * @param $otext String: old text, must be already segmented
         * @param $ntext String: new text, must be already segmented
 -       * @return bool|string
 +       * @deprecated since 1.WD, use generateContentDiffBody() instead!
         */
        function generateDiffBody( $otext, $ntext ) {
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              return $this->generateTextDiffBody( $otext, $ntext );
 +      }
 +
 +      /**
 +       * Generate a diff, no caching
 +       *
 +       * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point.
 +       *
 +       * @param $otext String: old text, must be already segmented
 +       * @param $ntext String: new text, must be already segmented
 +       * @return bool|string
 +       */
 +      function generateTextDiffBody( $otext, $ntext ) {
                global $wgExternalDiffEngine, $wgContLang;
  
                wfProfileIn( __METHOD__ );
         *        the visibility of the revision and a link to edit the page.
         * @return String HTML fragment
         */
 -      private function getRevisionHeader( Revision $rev, $complete = '' ) {
 +      protected function getRevisionHeader( Revision $rev, $complete = '' ) {
                $lang = $this->getLanguage();
                $user = $this->getUser();
                $revtimestamp = $rev->getTimestamp();
  
        /**
         * Use specified text instead of loading from the database
 +       * @deprecated since 1.WD, use setContent() instead.
         */
        function setText( $oldText, $newText ) {
 -              $this->mOldtext = $oldText;
 -              $this->mNewtext = $newText;
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() );
 +              $newContent = ContentHandler::makeContent( $newText, $this->getTitle() );
 +
 +              $this->setContent( $oldContent, $newContent );
 +      }
 +
 +      /**
 +       * Use specified text instead of loading from the database
 +       * @since 1.WD
 +       */
 +      function setContent( Content $oldContent, Content $newContent ) {
 +              $this->mOldContent = $oldContent;
 +              $this->mNewContent = $newContent;
 +
                $this->mTextLoaded = 2;
                $this->mRevisionsLoaded = true;
        }
                        return false;
                }
                if ( $this->mOldRev ) {
 -                      $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
 -                      if ( $this->mOldtext === false ) {
 +                      $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER );
 +                      if ( $this->mOldContent === false ) {
                                return false;
                        }
                }
                if ( $this->mNewRev ) {
 -                      $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
 -                      if ( $this->mNewtext === false ) {
 +                      $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
 +                      if ( $this->mNewContent === false ) {
                                return false;
                        }
                }
                if ( !$this->loadRevisionData() ) {
                        return false;
                }
 -              $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
 +              $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
                return true;
        }
  }
@@@ -163,7 -163,8 +163,8 @@@ class Parser 
        var $mLinkHolders;
  
        var $mLinkID;
-       var $mIncludeSizes, $mPPNodeCount, $mHighestExpansionDepth, $mDefaultSort;
+       var $mIncludeSizes, $mPPNodeCount, $mGeneratedPPNodeCount, $mHighestExpansionDepth;
+       var $mDefaultSort;
        var $mTplExpandCache; # empty-frame expansion cache
        var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
        var $mExpensiveFunctionCount; # number of expensive parser function calls
                        'arg' => 0,
                );
                $this->mPPNodeCount = 0;
+               $this->mGeneratedPPNodeCount = 0;
                $this->mHighestExpansionDepth = 0;
                $this->mDefaultSort = false;
                $this->mHeadings = array();
                 * to internalParse() which does all the real work.
                 */
  
-               global $wgUseTidy, $wgAlwaysUseTidy, $wgDisableLangConversion, $wgDisableTitleConversion;
+               global $wgUseTidy, $wgAlwaysUseTidy;
                $fname = __METHOD__.'-' . wfGetCaller();
                wfProfileIn( __METHOD__ );
                wfProfileIn( $fname );
                 * c) It's a conversion table
                 * d) it is an interface message (which is in the user language)
                 */
-               if ( !( $wgDisableLangConversion
-                               || isset( $this->mDoubleUnderscores['nocontentconvert'] )
-                               || $this->mTitle->isConversionTable() ) )
+               if ( !( $options->getDisableContentConversion()
+                               || isset( $this->mDoubleUnderscores['nocontentconvert'] ) ) )
                {
                        # Run convert unconditionally in 1.18-compatible mode
                        global $wgBug34832TransitionalRollback;
                 * {{DISPLAYTITLE:...}} is present. DISPLAYTITLE takes precedence over
                 * automatic link conversion.
                 */
-               if ( !( $wgDisableLangConversion
-                               || $wgDisableTitleConversion
+               if ( !( $options->getDisableTitleConversion()
                                || isset( $this->mDoubleUnderscores['nocontentconvert'] )
                                || isset( $this->mDoubleUnderscores['notitleconvert'] )
                                || $this->mOutput->getDisplayTitle() !== false ) )
                        $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/{$this->mOptions->getExpensiveParserFunctionLimit()}\n";
                        $limitReport =
                                "NewPP limit report\n" .
-                               "Preprocessor node count: {$this->mPPNodeCount}/{$this->mOptions->getMaxPPNodeCount()}\n" .
+                               "Preprocessor visited node count: {$this->mPPNodeCount}/{$this->mOptions->getMaxPPNodeCount()}\n" .
+                               "Preprocessor generated node count: " .
+                                       "{$this->mGeneratedPPNodeCount}/{$this->mOptions->getMaxGeneratedPPNodeCount()}\n" .
                                "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
                                "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n".
                                "Highest expansion depth: {$this->mHighestExpansionDepth}/{$this->mOptions->getMaxPPExpandDepth()}\n".
                                # Interwikis
                                wfProfileIn( __METHOD__."-interwiki" );
                                if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' ) ) {
 +                                      // FIXME: the above check prevents links to sites with identifiers that are not language codes
                                        $this->mOutput->addLanguageLink( $nt->getFullText() );
                                        $s = rtrim( $s . $prefix );
                                        $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
                        }
  
                        if ( $rev ) {
 -                              $text = $rev->getText();
 +                              $content = $rev->getContent();
 +                              $text = $content->getWikitextForTransclusion();
 +
 +                              if ( $text === false || $text === null ) {
 +                                      $text = false;
 +                                      break;
 +                              }
                        } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
                                global $wgContLang;
                                $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
                                        $text = false;
                                        break;
                                }
 +                              $content = $message->content();
                                $text = $message->plain();
                        } else {
                                break;
                        }
 -                      if ( $text === false ) {
 +                      if ( !$content ) {
                                break;
                        }
                        # Redirect?
                        $finalTitle = $title;
 -                      $title = Title::newFromRedirect( $text );
 +                      $title = $content->getRedirectTarget();
                }
                return array(
                        'text' => $text,
@@@ -32,16 -32,7 +32,16 @@@ class PageArchive 
         * @var Title
         */
        protected $title;
 -      var $fileStatus;
 +
 +      /**
 +       * @var Status
 +       */
 +      protected $fileStatus;
 +
 +      /**
 +       * @var Status
 +       */
 +      protected $revisionStatus;
  
        function __construct( $title ) {
                if( is_null( $title ) ) {
         * @return ResultWrapper
         */
        function listRevisions() {
 +              global $wgContentHandlerNoDB;
 +
                $dbr = wfGetDB( DB_SLAVE );
 +
 +              $fields = array(
 +                      'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
 +                      'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
 +              );
 +
 +              if ( !$wgContentHandlerNoDB ) {
 +                      $fields[] = 'ar_content_format';
 +                      $fields[] = 'ar_content_model';
 +              }
 +
                $res = $dbr->select( 'archive',
 -                      array(
 -                              'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
 -                              'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1'
 -                      ),
 +                      $fields,
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                   'ar_title' => $this->title->getDBkey() ),
                        __METHOD__,
         * @return Revision
         */
        function getRevision( $timestamp ) {
 +              global $wgContentHandlerNoDB;
 +
                $dbr = wfGetDB( DB_SLAVE );
 +
 +              $fields = array(
 +                      'ar_rev_id',
 +                      'ar_text',
 +                      'ar_comment',
 +                      'ar_user',
 +                      'ar_user_text',
 +                      'ar_timestamp',
 +                      'ar_minor_edit',
 +                      'ar_flags',
 +                      'ar_text_id',
 +                      'ar_deleted',
 +                      'ar_len',
 +                      'ar_sha1',
 +              );
 +
 +              if ( !$wgContentHandlerNoDB ) {
 +                      $fields[] = 'ar_content_format';
 +                      $fields[] = 'ar_content_model';
 +              }
 +
                $row = $dbr->selectRow( 'archive',
 -                      array(
 -                              'ar_rev_id',
 -                              'ar_text',
 -                              'ar_comment',
 -                              'ar_user',
 -                              'ar_user_text',
 -                              'ar_timestamp',
 -                              'ar_minor_edit',
 -                              'ar_flags',
 -                              'ar_text_id',
 -                              'ar_deleted',
 -                              'ar_len',
 -                              'ar_sha1',
 -                      ),
 +                      $fields,
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                        'ar_title' => $this->title->getDBkey(),
                                        'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
                        __METHOD__ );
                if( $row ) {
 -                      return Revision::newFromArchiveRow( $row, array( 'page' => $this->title->getArticleID() ) );
 +                      return Revision::newFromArchiveRow( $row, array( 'title' => $this->title ) );
                } else {
                        return null;
                }
                if( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
                        $img = wfLocalFile( $this->title );
                        $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
 -                      if ( !$this->fileStatus->isOk() ) {
 +                      if ( !$this->fileStatus->isOK() ) {
                                return false;
                        }
                        $filesRestored = $this->fileStatus->successCount;
                }
  
                if( $restoreText ) {
 -                      $textRestored = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
 -                      if( $textRestored === false ) { // It must be one of UNDELETE_*
 +                      $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
 +                      if( !$this->revisionStatus->isOK() ) {
                                return false;
                        }
 +
 +                      $textRestored = $this->revisionStatus->getValue();
                } else {
                        $textRestored = 0;
                }
         * @param $comment String
         * @param $unsuppress Boolean: remove all ar_deleted/fa_deleted restrictions of seletected revs
         *
 -       * @return Mixed: number of revisions restored or false on failure
 +       * @return Status, containing the number of revisions restored on success
         */
        private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
 +              global $wgContentHandlerNoDB;
 +
                if ( wfReadOnly() ) {
 -                      return false;
 +                      throw new ReadOnlyError();
                }
                $restoreAll = empty( $timestamps );
  
                        $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
                                array( 'rev_id' => $previousRevId ),
                                __METHOD__ );
 +
                        if( $previousTimestamp === false ) {
                                wfDebug( __METHOD__.": existing page refers to a page_latest that does not exist\n" );
 -                              return 0;
 +
 +                              $status = Status::newGood( 0 );
 +                              $status->warning( 'undeleterevision-missing' );
 +
 +                              return $status;
                        }
                } else {
                        # Have to create a new article...
                        $oldones = "ar_timestamp IN ( {$oldts} )";
                }
  
 +              $fields = array(
 +                      'ar_rev_id',
 +                      'ar_text',
 +                      'ar_comment',
 +                      'ar_user',
 +                      'ar_user_text',
 +                      'ar_timestamp',
 +                      'ar_minor_edit',
 +                      'ar_flags',
 +                      'ar_text_id',
 +                      'ar_deleted',
 +                      'ar_page_id',
 +                      'ar_len',
 +                      'ar_sha1');
 +
 +              if ( !$wgContentHandlerNoDB ) {
 +                      $fields[] = 'ar_content_format';
 +                      $fields[] = 'ar_content_model';
 +              }
 +
                /**
                 * Select each archived revision...
                 */
                $result = $dbw->select( 'archive',
 -                      /* fields */ array(
 -                              'ar_rev_id',
 -                              'ar_text',
 -                              'ar_comment',
 -                              'ar_user',
 -                              'ar_user_text',
 -                              'ar_timestamp',
 -                              'ar_minor_edit',
 -                              'ar_flags',
 -                              'ar_text_id',
 -                              'ar_deleted',
 -                              'ar_page_id',
 -                              'ar_len',
 -                              'ar_sha1' ),
 +                      $fields,
                        /* WHERE */ array(
                                'ar_namespace' => $this->title->getNamespace(),
                                'ar_title'     => $this->title->getDBkey(),
                $rev_count = $dbw->numRows( $result );
                if( !$rev_count ) {
                        wfDebug( __METHOD__ . ": no revisions to restore\n" );
 -                      return false; // ???
 +
 +                      $status = Status::newGood( 0 );
 +                      $status->warning( "undelete-no-results" );
 +                      return $status;
                }
  
                $ret->seek( $rev_count - 1 ); // move to last
                $row = $ret->fetchObject(); // get newest archived rev
                $ret->seek( 0 ); // move back
  
 +              // grab the content to check consistency with global state before restoring the page.
 +              $revision = Revision::newFromArchiveRow( $row,
 +                      array(
 +                              'title' => $article->getTitle(), // used to derive default content model
 +                      ) );
 +
 +              $m = $revision->getContentModel();
 +
 +              $user = User::newFromName( $revision->getRawUserText(), false );
 +              $content = $revision->getContent( Revision::RAW );
 +
 +              //NOTE: article ID may not be known yet. prepareSave() should not modify the database.
 +              $status = $content->prepareSave( $article, 0, -1, $user );
 +
 +              if ( !$status->isOK() ) {
 +                      return $status;
 +              }
 +
                if( $makepage ) {
                        // Check the state of the newest to-be version...
                        if( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) {
 -                              return false; // we can't leave the current revision like this!
 +                              return Status::newFatal( "undeleterevdel" );
                        }
                        // Safe to insert now...
                        $newid  = $article->insertOn( $dbw );
                        if( $row->ar_timestamp > $previousTimestamp ) {
                                // Check the state of the newest to-be version...
                                if( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) {
 -                                      return false; // we can't leave the current revision like this!
 +                                      return Status::newFatal( "undeleterevdel" );
                                }
                        }
                }
                        // unless we are specifically removing all restrictions...
                        $revision = Revision::newFromArchiveRow( $row,
                                array(
 -                                      'page' => $pageId,
 +                                      'title' => $this->title,
                                        'deleted' => $unsuppress ? 0 : $row->ar_deleted
                                ) );
  
  
                // Was anything restored at all?
                if ( $restored == 0 ) {
 -                      return 0;
 +                      return Status::newGood( 0 );
                }
  
                $created = (bool)$newid;
                        $update->doUpdate();
                }
  
 -              return $restored;
 +              return Status::newGood( $restored );
        }
  
        /**
         * @return Status
         */
        function getFileStatus() { return $this->fileStatus; }
 +
 +      /**
 +       * @return Status
 +       */
 +      function getRevisionStatus() { return $this->revisionStatus; }
  }
  
  /**
@@@ -851,13 -780,11 +851,13 @@@ class SpecialUndelete extends SpecialPa
  
        private function showRevision( $timestamp ) {
                if( !preg_match( '/[0-9]{14}/', $timestamp ) ) {
 -                      return 0;
 +                      return;
                }
  
                $archive = new PageArchive( $this->mTargetObj );
 -              wfRunHooks( 'UndeleteForm::showRevision', array( &$archive, $this->mTargetObj ) );
 +              if ( !wfRunHooks( 'UndeleteForm::showRevision', array( &$archive, $this->mTargetObj ) ) ) {
 +                      return;
 +              }
                $rev = $archive->getRevision( $timestamp );
  
                $out = $this->getOutput();
                $t = $lang->userTime( $timestamp, $user );
                $userLink = Linker::revUserTools( $rev );
  
 -              if( $this->mPreview ) {
 +              $content = $rev->getContent( Revision::FOR_THIS_USER, $user );
 +
 +              $isText = ( $content instanceof TextContent );
 +
 +              if( $this->mPreview || $isText ) {
                        $openDiv = '<div id="mw-undelete-revision" class="mw-warning">';
                } else {
                        $openDiv = '<div id="mw-undelete-revision">';
  
                $out->addHTML( $this->msg( 'undelete-revision' )->rawParams( $link )->params(
                        $time )->rawParams( $userLink )->params( $d, $t )->parse() . '</div>' );
 -              wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) );
  
 -              if( $this->mPreview ) {
 +              if ( !wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) ) ) {
 +                      return;
 +              }
 +
 +              if( $this->mPreview || !$isText ) {
 +                      // NOTE: non-text content has no source view, so always use rendered preview
 +
                        // Hide [edit]s
                        $popts = $out->parserOptions();
                        $popts->setEditSection( false );
 -                      $out->parserOptions( $popts );
 -                      $out->addWikiTextTitleTidy( $rev->getText( Revision::FOR_THIS_USER, $user ), $this->mTargetObj, true );
 +
 +                      $pout = $content->getParserOutput( $this->mTargetObj, $rev->getId(), $popts, true );
 +                      $out->addParserOutput( $pout );
                }
  
 +              if ( $isText ) {
 +                      // source view for textual content
 +                      $sourceView = Xml::element( 'textarea', array(
 +                              'readonly' => 'readonly',
 +                              'cols' => intval( $user->getOption( 'cols' ) ),
 +                              'rows' => intval( $user->getOption( 'rows' ) ) ),
 +                              $content->getNativeData() . "\n" );
 +
 +                      $previewButton = Xml::element( 'input', array(
 +                              'type' => 'submit',
 +                              'name' => 'preview',
 +                              'value' => $this->msg( 'showpreview' )->text() ) );
 +              } else {
 +                      $sourceView = '';
 +                      $previewButton = '';
 +              }
 +
 +              $diffButton = Xml::element( 'input', array(
 +                      'name' => 'diff',
 +                      'type' => 'submit',
 +                      'value' => $this->msg( 'showdiff' )->text() ) );
 +
                $out->addHTML(
 -                      Xml::element( 'textarea', array(
 -                                      'readonly' => 'readonly',
 -                                      'cols' => intval( $user->getOption( 'cols' ) ),
 -                                      'rows' => intval( $user->getOption( 'rows' ) ) ),
 -                              $rev->getText( Revision::FOR_THIS_USER, $user ) . "\n" ) .
 -                      Xml::openElement( 'div' ) .
 +                      $sourceView .
 +                      Xml::openElement( 'div', array(
 +                              'style' => 'clear: both' ) ) .
                        Xml::openElement( 'form', array(
                                'method' => 'post',
                                'action' => $this->getTitle()->getLocalURL( array( 'action' => 'submit' ) ) ) ) .
                                'type' => 'hidden',
                                'name' => 'wpEditToken',
                                'value' => $user->getEditToken() ) ) .
 -                      Xml::element( 'input', array(
 -                              'type' => 'submit',
 -                              'name' => 'preview',
 -                              'value' => $this->msg( 'showpreview' )->text() ) ) .
 -                      Xml::element( 'input', array(
 -                              'name' => 'diff',
 -                              'type' => 'submit',
 -                              'value' => $this->msg( 'showdiff' )->text() ) ) .
 +                      $previewButton .
 +                      $diffButton .
                        Xml::closeElement( 'form' ) .
                        Xml::closeElement( 'div' ) );
        }
         * @return String: HTML
         */
        function showDiff( $previousRev, $currentRev ) {
 -              $diffEngine = new DifferenceEngine( $this->getContext() );
 +              $diffContext = clone $this->getContext();
 +              $diffContext->setTitle( $currentRev->getTitle() );
 +              $diffContext->setWikiPage( WikiPage::factory( $currentRev->getTitle() ) );
 +
 +              $diffEngine = $currentRev->getContentHandler()->createDifferenceEngine( $diffContext );
                $diffEngine->showDiffStyle();
                $this->getOutput()->addHTML(
                        "<div>" .
-                       "<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
+                       "<table width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
                        "<col class='diff-marker' />" .
                        "<col class='diff-content' />" .
                        "<col class='diff-marker' />" .
                                $this->diffHeader( $currentRev, 'n' ) .
                                "</td>\n" .
                        "</tr>" .
 -                      $diffEngine->generateDiffBody(
 -                              $previousRev->getText( Revision::FOR_THIS_USER, $this->getUser() ),
 -                              $currentRev->getText( Revision::FOR_THIS_USER, $this->getUser() ) ) .
 +                      $diffEngine->generateContentDiffBody(
 +                              $previousRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ),
 +                              $currentRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ) ) .
                        "</table>" .
                        "</div>\n"
                );
  
        private function formatRevisionRow( $row, $earliestLiveTime, $remaining ) {
                $rev = Revision::newFromArchiveRow( $row,
 -                      array( 'page' => $this->mTargetObj->getArticleID() ) );
 +                      array(
 +                              'title' => $this->mTargetObj
 +                      ) );
 +
                $revTextSize = '';
                $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
                // Build checkboxen...
                        $out->addHTML( $this->msg( 'undeletedpage' )->rawParams( $link )->parse() );
                } else {
                        $out->setPageTitle( $this->msg( 'undelete-error' ) );
 -                      $out->addWikiMsg( 'cannotundelete' );
 -                      $out->addWikiMsg( 'undeleterevdel' );
                }
  
 -              // Show file deletion warnings and errors
 +              // Show revision undeletion warnings and errors
 +              $status = $archive->getRevisionStatus();
 +              if( $status && !$status->isGood() ) {
 +                      $out->addWikiText( '<div class="error">' . $status->getWikiText( 'cannotundelete', 'cannotundelete' ) . '</div>' );
 +              }
 +
 +              // Show file undeletion warnings and errors
                $status = $archive->getFileStatus();
                if( $status && !$status->isGood() ) {
                        $out->addWikiText( '<div class="error">' . $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) . '</div>' );
diff --combined languages/Language.php
@@@ -419,16 -419,6 +419,16 @@@ class Language 
         */
        public function setNamespaces( array $namespaces ) {
                $this->namespaceNames = $namespaces;
 +              $this->mNamespaceIds = null;
 +      }
 +
 +      /**
 +       * Resets all of the namespace caches. Mainly used for testing
 +       */
 +      public function resetNamespaces( ) {
 +              $this->namespaceNames = null;
 +              $this->mNamespaceIds = null;
 +              $this->namespaceAliases = null;
        }
  
        /**
        function listToText( array $l ) {
                $s = '';
                $m = count( $l ) - 1;
-               
                if ( $m === 0 ) {
                        return $l[0];
                } elseif ( $m === 1 ) {
         * @return array Associative array with plural form, and plural rule as key-value pairs
         */
        public function getCompiledPluralRules() {
-               return self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
+               $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
+               $fallbacks = Language::getFallbacksFor( $this->mCode );
+               if ( !$pluralRules ) {
+                       foreach ( $fallbacks as $fallbackCode ) {
+                               $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
+                               if ( $pluralRules ) {
+                                       break;
+                               }
+                       }
+               }
+               return $pluralRules;
        }
  
        /**
         * @return array Associative array with plural form, and plural rule as key-value pairs
         */
        public function getPluralRules() {
-               return self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
+               $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
+               $fallbacks = Language::getFallbacksFor( $this->mCode );
+               if ( !$pluralRules ) {
+                       foreach ( $fallbacks as $fallbackCode ) {
+                               $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
+                               if ( $pluralRules ) {
+                                       break;
+                               }
+                       }
+               }
+               return $pluralRules;
        }
  
        /**
@@@ -780,10 -780,11 +780,11 @@@ Bitte versuche es in ein paar Minuten e
  'protectedpagetext' => 'Diese Seite wurde geschützt, um Bearbeitungen zu verhindern.',
  'viewsourcetext' => 'Du kannst den Quelltext dieser Seite betrachten und kopieren:',
  'viewyourtext' => "Du kannst den Quelltext '''deiner Bearbeitung''' dieser Seite betrachten und kopieren:",
- 'protectedinterface' => 'Diese Seite enthält Text für die Benutzeroberfläche der Software und ist geschützt, um Missbrauch vorzubeugen.',
+ 'protectedinterface' => 'Diese Seite enthält Text für die Benutzeroberfläche der Software auf diesem Wiki und ist geschützt, um Missbrauch vorzubeugen.
+ Nutze bitte [//translatewiki.net/ translatewiki.net], das Lokalisierungsprojekt von MediaWiki, um Übersetzungen für alle Wikis hinzuzufügen oder zu ändern.',
  'editinginterface' => "'''Warnung:''' Diese Seite enthält von der MediaWiki-Software genutzten Text.
- Änderungen auf dieser Seite wirken sich auf die Benutzeroberfläche aus.
Ziehe bitte im Fall von Übersetzungen in Betracht, diese bei [//translatewiki.net/wiki/Main_Page?setlang=de translatewiki.net], der Lokalisierungsplattform für MediaWiki, durchzuführen.",
+ Änderungen auf dieser Seite wirken sich auf die Benutzeroberfläche dieses Wikis aus.
Nutze bitte [//translatewiki.net/ translatewiki.net], das Lokalisierungsprojekt von MediaWiki, um Übersetzungen für alle Wikis hinzuzufügen oder zu ändern.",
  'sqlhidden' => "''Die SQL-Datenbankabfrage ist verborgen.''",
  'cascadeprotected' => 'Diese Seite ist zur Bearbeitung gesperrt. Sie ist in die {{PLURAL:$1|folgende Seite|folgenden Seiten}} eingebunden, die mittels der Kaskadensperroption geschützt {{PLURAL:$1|ist|sind}}:
  $2',
@@@ -1939,7 -1940,7 +1940,7 @@@ Wenn das Problem weiter besteht, inform
  'backend-fail-internal' => 'Im Speicher-Backend „$1“ ist ein unbekannter Fehler aufgetreten.',
  'backend-fail-contenttype' => 'Der Inhaltstyp, der im Pfad „$1“ zu speichernden Datei, konnte nicht bestimmt werden.',
  'backend-fail-batchsize' => 'Eine Stapelverarbeitungsdatei, die {{PLURAL:$1|eine Operation|$1 Operationen}} enthält, wurde an das Speicher-Backend gesandt. Die Begrenzung liegt allerdings bei {{PLURAL:$2|einer Operation|$2 Operationen}}.',
- 'backend-fail-usable' => 'Die Datei $1 konnte, entweder aufgrund eines nicht vorhandenen Verzeichnisses oder aufgrund unzureichender Berechtigungen, nicht gespeichert werden.',
+ 'backend-fail-usable' => 'Die Datei „$1“ konnte, entweder aufgrund eines nicht vorhandenen Verzeichnisses oder aufgrund unzureichender Berechtigungen, weder abgerufen noch gespeichert werden.',
  
  # File journal errors
  'filejournal-fail-dbconnect' => 'Es konnte keine Verbindung zur Journaldatenbank des Speicher-Backends „$1“ hergestellt werden.',
@@@ -2602,8 -2603,7 +2603,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.",
@@@ -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.',
@@@ -1038,10 -1037,11 +1038,11 @@@ Please try again in a few minutes.'
  'protectedpagetext'             => 'This page has been protected to prevent editing.',
  'viewsourcetext'                => 'You can view and copy the source of this page:',
  'viewyourtext'                  => "You can view and copy the source of '''your edits''' to this page:",
- 'protectedinterface'            => 'This page provides interface text for the software, and is protected to prevent abuse.',
+ 'protectedinterface'            => 'This page provides interface text for the software on this wiki, and is protected to prevent abuse.
+ To add or change translations for all wikis, please use [//translatewiki.net/ translatewiki.net], the MediaWiki localisation project.',
  'editinginterface'              => "'''Warning:''' You are editing a page which is used to provide interface text for the software.
- Changes to this page will affect the appearance of the user interface for other users.
For translations, please consider using [//translatewiki.net/wiki/Main_Page?setlang=en translatewiki.net], the MediaWiki localisation project.",
+ Changes to this page will affect the appearance of the user interface for other users on this wiki.
To add or change translations for all wikis, please use [//translatewiki.net/ translatewiki.net], the MediaWiki localisation project.",
  'sqlhidden'                     => '(SQL query hidden)',
  'cascadeprotected'              => 'This page has been protected from editing, because it is included in the following {{PLURAL:$1|page, which is|pages, which are}} protected with the "cascading" option turned on:
  $2',
@@@ -1430,7 -1430,7 +1431,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,8 -1487,6 +1488,8 @@@ It already exists.'
  'addsection-preload'               => '', # do not translate or duplicate this message to other languages
  'addsection-editintro'             => '', # do not translate or duplicate this message to other languages
  'defaultmessagetext'               => 'Default message text',
 +'invalid-content-data'             => 'Invalid content data',
 +'content-not-allowed-here'         => '"$1" content is not allowed on page [[$2]]',
  
  # Parser/template warnings
  'expensive-parserfunction-warning'        => "'''Warning:''' This page contains too many expensive parser function calls.
@@@ -2325,7 -2323,7 +2326,7 @@@ If the problem persists, contact an [[S
  'backend-fail-internal'      => 'An unknown error occurred in storage backend "$1".',
  'backend-fail-contenttype'   => 'Could not determine the content type of the file to store at "$1".',
  'backend-fail-batchsize'     => 'The storage backend was given a batch of $1 file {{PLURAL:$1|operation|operations}}; the limit is $2 {{PLURAL:$2|operation|operations}}.',
- 'backend-fail-usable'        => 'Could not write file "$1" due to insufficient permissions or missing directories/containers.',
+ 'backend-fail-usable'        => 'Could not read or write file "$1" due to insufficient permissions or missing directories/containers.',
  
  # File journal errors
  'filejournal-fail-dbconnect' => 'Could not connect to the journal database for storage backend "$1".',
@@@ -3072,8 -3070,8 +3073,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.",
@@@ -4946,10 -4944,4 +4947,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',
 +
  );
@@@ -706,7 -706,7 +706,7 @@@ $1 is a filename, I think.'
  'viewsourcetext' => 'The text shown when displaying the source of a page that the user has no permission to edit',
  'viewyourtext' => 'Same as {{msg-mw|viewsourcetext}} but when showing the text submitted by the user, this happens e.g. when the user was blocked while he is editing the page',
  'protectedinterface' => 'Message shown if a user without the "editinterface" right tries to edit a page in the MediaWiki namespace.',
- 'editinginterface' => "A message shown when editing pages in the namespace MediaWiki:. In the [http://translatewiki.net/wiki/Main_Page?setlang=en URL], '''change \"setlang=en\" to your own language code.'''",
+ 'editinginterface' => 'A message shown when editing pages in the namespace MediaWiki:.',
  'ns-specialprotected' => 'Error message displayed when trying to edit a page in the Special namespace',
  'titleprotected' => 'Use $1 for GENDER.',
  'invalidtitle-knownnamespace' => 'Displayed when an invalid title was encountered (generally in a list), but the namespace number is known to exist.
@@@ -1053,10 -1053,6 +1053,10 @@@ Please report at [[Support]] if you ar
  'moveddeleted-notice' => 'Shown on top of a deleted page in normal view modus ([http://translatewiki.net/wiki/Test example]).',
  'edit-conflict' => "An 'Edit conflict' happens when more than one edit is being made to a page at the same time. This would usually be caused by separate individuals working on the same page. However, if the system is slow, several edits from one individual could back up and attempt to apply simultaneously - causing the conflict.",
  'defaultmessagetext' => 'Caption above the default message text shown on the left-hand side of a diff displayed after clicking “Show changes” when creating a new page in the MediaWiki: namespace',
 +'invalid-content-data'             => 'Error message indicating that the page\'s content can not be saved because it is invalid. This may occurr for some non-text content types.',
 +'content-not-allowed-here'         => 'Error message indicating that the desired content model is not supported in given localtion.
 +* $1 is the human readable name of the content model
 +* $1 is the title of the page in question.',
  
  # Parser/template warnings
  'expensive-parserfunction-warning' => 'On some (expensive) [[MetaWikipedia:Help:ParserFunctions|parser functions]] (e.g. <code><nowiki>{{#ifexist:}}</nowiki></code>) there is a limit of how many times it may be used. This is an error message shown when the limit is exceeded.
@@@ -1409,8 -1405,11 +1409,11 @@@ This is a search result (and I guess se
  
  This is a search result (and I guess search engine) dependent messages. I do not know how to trigger the feature. The message is displayed if the search result contains information that related pages can also be provided from the search engine. I assume this is "More Like This" functionality. Microsoft glossary defines MLT as "A way to refine search by identifying the right set of documents and then locating similar documents. This allows the searcher to control the direction of the search and focus on the most fruitful lines of inquiry."[http://www.microsoft.com/enterprisesearch/en/us/search-glossary.aspx]',
  'searchall' => '{{Identical|All}}',
- 'showingresults' => "This message is used on some special pages such as 'Wanted categories'. $1 is the total number of results in the batch shown and $2 is the number of the first item listed.",
- 'showingresultsnum' => '$3 is the number of results on the page and $2 is the first number in the batch of results.',
+ 'showingresults' => "This message is used on some special pages such as 'Wanted categories'.
+ *$1 is the total number of results in the batch shown.
+ *$2 is the number of the first item listed.",
+ 'showingresultsnum' => '*$3 is the number of results on the page.
+ *$2 is the first number in the batch of results.',
  'showingresultsheader' => 'Used in search results of [[Special:Search]].',
  'nonefound' => 'This message appears on the search results page if no results are found.
  {{doc-important|Do not translate "all:".}}',
@@@ -2883,8 -2882,6 +2886,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',
@@@ -3559,7 -3556,10 +3562,10 @@@ Parameters
  'svg-long-desc' => 'Displayed under an SVG image at the image description page. Note that argument 3 is a string that includes the file size unit symbol. See for example [[:File:Yes check.svg]].
  
  Start with a lowercase letter, unless the first word is “SVG”.',
- 'svg-long-desc-animated' => 'Displayed under an SVG image at the image description page if the image is animated. Non-animated images use {{msg-mw|svg-long-desc}}. $1 is width, $2 is height, and $3 is file size, including unit (for example "10 KB").
+ 'svg-long-desc-animated' => 'Displayed under an SVG image at the image description page if the image is animated. Non-animated images use {{msg-mw|svg-long-desc}}.
+ * $1 is the width in pixels
+ * $2 is the height in pixels, and 
+ * $3 is the file size including a unit (for example "10 KB").
  
  Start with a lowercase letter, unless the first word is “SVG”.',
  'show-big-image' => 'Displayed under an image at the image description page, when it is displayed smaller there than it was uploaded.',
@@@ -4883,10 -4883,4 +4889,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.',
 +
  );
@@@ -1068,7 -1068,7 +1068,7 @@@ abstract class Maintenance 
         */
        private function lockSearchindex( &$db ) {
                $write = array( 'searchindex' );
-               $read = array( 'page', 'revision', 'text', 'interwiki', 'l10n_cache' );
+               $read = array( 'page', 'revision', 'text', 'interwiki', 'l10n_cache', 'user' );
                $db->lockTables( $read, $write, __CLASS__ . '::' . __METHOD__ );
        }
  
                        $title = $titleObj->getPrefixedDBkey();
                        $this->output( "$title..." );
                        # Update searchindex
 -                      $u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getText() );
 +                      # TODO: pass the Content object to SearchUpdate, let the search engine decide how to deal with it.
 +                      $u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getContent()->getTextForSearchIndex() );
                        $u->doUpdate();
                        $this->output( "\n" );
                }
@@@ -5,6 -5,11 +5,11 @@@ abstract class MediaWikiTestCase extend
        public $regex = '';
        public $runDisabled = false;
  
+       /**
+        * @var Array of TestUser
+        */
+       public static $users;
        /**
         * @var DatabaseBase
         */
        protected $reuseDB = false;
        protected $tablesUsed = array(); // tables with data
  
 +      protected $restoreGlobals = array( // global variables to restore for each test
 +              'wgLang',
 +              'wgContLang',
 +              'wgLanguageCode',
 +              'wgUser',
 +              'wgTitle',
 +      );
 +
 +      private $savedGlobals = array();
 +
        private static $dbSetup = false;
  
        /**
                return $fname;
        }
  
 -      protected function tearDown() {
 +      protected function setup() {
 +              parent::setup();
 +
 +              foreach ( $this->restoreGlobals as $var ) {
 +                      $v = $GLOBALS[ $var ];
 +
 +                      if ( is_object( $v ) || is_array( $v ) ) {
 +                              $v = clone $v;
 +                      }
 +
 +                      $this->savedGlobals[ $var ] = $v;
 +              }
 +      }
 +
 +      protected function teardown() {
                // Cleaning up temporary files
                foreach ( $this->tmpfiles as $fname ) {
                        if ( is_file( $fname ) || ( is_link( $fname ) ) ) {
                        }
                }
  
 -              parent::tearDown();
 +              // restore saved globals
 +              foreach ( $this->savedGlobals as $k => $v ) {
 +                      $GLOBALS[ $k ] = $v;
 +              }
 +
 +              parent::teardown();
        }
  
        function dbPrefix() {
                //Make 1 page with 1 revision
                $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
                if ( !$page->getId() == 0 ) {
 -                      $page->doEdit( 'UTContent',
 -                                                      'UTPageSummary',
 -                                                      EDIT_NEW,
 -                                                      false,
 -                                                      User::newFromName( 'UTSysop' ) );
 +                      $page->doEditContent(
 +                              new WikitextContent( 'UTContent' ),
 +                              'UTPageSummary',
 +                              EDIT_NEW,
 +                              false,
 +                              User::newFromName( 'UTSysop' ) );
                }
        }
  
                }
        }
  
+       /**
+        * Asserts that the provided variable is of the specified
+        * internal type or equals the $value argument. This is useful
+        * for testing return types of functions that return a certain
+        * type or *value* when not set or on error.
+        *
+        * @since 1.20
+        *
+        * @param string $type
+        * @param mixed $actual
+        * @param mixed $value
+        * @param string $message
+        */
+       protected function assertTypeOrValue( $type, $actual, $value = false, $message = '' ) {
+               if ( $actual === $value ) {
+                       $this->assertTrue( true, $message );
+               }
+               else {
+                       $this->assertType( $type, $actual, $message );
+               }
+       }
+       /**
+        * Asserts the type of the provided value. This can be either
+        * in internal type such as boolean or integer, or a class or
+        * interface the value extends or implements.
+        *
+        * @since 1.20
+        *
+        * @param string $type
+        * @param mixed $actual
+        * @param string $message
+        */
+       protected function assertType( $type, $actual, $message = '' ) {
+               if ( is_object( $actual ) ) {
+                       $this->assertInstanceOf( $type, $actual, $message );
+               }
+               else {
+                       $this->assertInternalType( $type, $actual, $message );
+               }
+       }
  }
@@@ -3,15 -3,14 +3,18 @@@
  /**
   * Test class for Revision storage.
   *
 + * @group ContentHandler
   * @group Database
   * ^--- important, causes temporary tables to be used instead of the real database
+  *
+  * @group medium
+  * ^--- important, causes tests not to fail with timeout
   */
  class RevisionStorageTest extends MediaWikiTestCase {
  
 +      /**
 +       * @var WikiPage $the_page
 +       */
        var $the_page;
  
        function  __construct( $name = null, array $data = array(), $dataName = '' ) {
        }
  
        public function setUp() {
 +              global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
 +
 +              $wgExtraNamespaces[ 12312 ] = 'Dummy';
 +              $wgExtraNamespaces[ 12313 ] = 'Dummy_talk';
 +
 +              $wgNamespaceContentModels[ 12312 ] = 'DUMMY';
 +              $wgContentHandlers[ 'DUMMY' ] = 'DummyContentHandlerForTesting';
 +
 +              MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
 +              $wgContLang->resetNamespaces(); # reset namespace cache
 +
                if ( !$this->the_page ) {
                        $this->the_page = $this->createPage( 'RevisionStorageTest_the_page', "just a dummy page" );
                }
        }
  
 +      public function tearDown() {
 +              global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
 +
 +              unset( $wgExtraNamespaces[ 12312 ] );
 +              unset( $wgExtraNamespaces[ 12313 ] );
 +
 +              unset( $wgNamespaceContentModels[ 12312 ] );
 +              unset( $wgContentHandlers[ 'DUMMY' ] );
 +
 +              MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
 +              $wgContLang->resetNamespaces(); # reset namespace cache
 +      }
 +
        protected function makeRevision( $props = null ) {
                if ( $props === null ) $props = array();
  
@@@ -88,8 -63,7 +91,8 @@@
                        $page->doDeleteArticle( "done" );
                }
  
 -              $page->doEdit( $text, "testing", EDIT_NEW );
 +              $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
 +              $page->doEditContent( $content, "testing", EDIT_NEW );
  
                return $page;
        }
                $this->assertEquals( $orig->getPage(), $rev->getPage() );
                $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() );
                $this->assertEquals( $orig->getUser(), $rev->getUser() );
 +              $this->assertEquals( $orig->getContentModel(), $rev->getContentModel() );
 +              $this->assertEquals( $orig->getContentFormat(), $rev->getContentFormat() );
                $this->assertEquals( $orig->getSha1(), $rev->getSha1() );
        }
  
                $page = $this->createPage( 'RevisionStorageTest_testFetchRevision', 'one' );
                $id1 = $page->getRevision()->getId();
  
 -              $page->doEdit( 'two', 'second rev' );
 +              $page->doEditContent( new WikitextContent( 'two' ), 'second rev' );
                $id2 = $page->getRevision()->getId();
  
                $res = Revision::fetchRevision( $page->getTitle() );
         */
        public function testSelectFields()
        {
 +              global $wgContentHandlerUseDB;
 +
                $fields = Revision::selectFields();
  
                $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields');
                $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields');
                $this->assertTrue( in_array( 'rev_timestamp', $fields ), 'missing rev_timestamp in list of fields');
                $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields');
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $this->assertTrue( in_array( 'rev_content_model', $fields ),
 +                                                              'missing rev_content_model in list of fields');
 +                      $this->assertTrue( in_array( 'rev_content_format', $fields ),
 +                                                              'missing rev_content_format in list of fields');
 +              } else {
 +                      $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
 +              }
        }
  
        /**
         */
        public function testGetText()
        {
 +              $this->hideDeprecated( 'Revision::getText' );
 +
                $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) );
                $rev = Revision::newFromId( $orig->getId() );
  
                $this->assertEquals( 'hello hello.', $rev->getText() );
        }
  
 +      /**
 +       * @covers Revision::getContent
 +       */
 +      public function testGetContent()
 +      {
 +              $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) );
 +              $rev = Revision::newFromId( $orig->getId() );
 +
 +              $this->assertEquals( 'hello hello.', $rev->getContent()->getNativeData() );
 +      }
 +
        /**
         * @covers Revision::revText
         */
         */
        public function testGetRawText()
        {
 +              $this->hideDeprecated( 'Revision::getRawText' );
 +
                $orig = $this->makeRevision( array( 'text' => 'hello hello raw.' ) );
                $rev = Revision::newFromId( $orig->getId() );
  
                $this->assertEquals( 'hello hello raw.', $rev->getRawText() );
        }
 +
 +      /**
 +       * @covers Revision::getContentModel
 +       */
 +      public function testGetContentModel()
 +      {
 +              global $wgContentHandlerUseDB;
 +
 +              if ( !$wgContentHandlerUseDB ) {
 +                      $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
 +              }
 +
 +              $orig = $this->makeRevision( array( 'text' => 'hello hello.',
 +                                                                                      'content_model' => CONTENT_MODEL_JAVASCRIPT ) );
 +              $rev = Revision::newFromId( $orig->getId() );
 +
 +              $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
 +      }
 +
 +      /**
 +       * @covers Revision::getContentFormat
 +       */
 +      public function testGetContentFormat()
 +      {
 +              global $wgContentHandlerUseDB;
 +
 +              if ( !$wgContentHandlerUseDB ) {
 +                      $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
 +              }
 +
 +              $orig = $this->makeRevision( array( 'text' => 'hello hello.',
 +                                                                                      'content_model' => CONTENT_MODEL_JAVASCRIPT,
 +                                                                                      'content_format' => CONTENT_FORMAT_JAVASCRIPT ) );
 +              $rev = Revision::newFromId( $orig->getId() );
 +
 +              $this->assertEquals( CONTENT_FORMAT_JAVASCRIPT, $rev->getContentFormat() );
 +      }
 +
        /**
         * @covers Revision::isCurrent
         */
                $rev1x = Revision::newFromId( $rev1->getId() );
                $this->assertTrue( $rev1x->isCurrent() );
  
 -              $page->doEdit( 'Bla bla', 'second rev' );
 +              $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle() ), 'second rev' );
                $rev2 = $page->getRevision();
  
                # @todo: find out if this should be true
  
                $this->assertNull( $rev1->getPrevious() );
  
 -              $page->doEdit( 'Bla bla', 'second rev testGetPrevious' );
 +              $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle() ),
 +                                                              'second rev testGetPrevious' );
                $rev2 = $page->getRevision();
  
                $this->assertNotNull( $rev2->getPrevious() );
  
                $this->assertNull( $rev1->getNext() );
  
 -              $page->doEdit( 'Bla bla', 'second rev testGetNext' );
 +              $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle() ),
 +                                                              'second rev testGetNext' );
                $rev2 = $page->getRevision();
  
                $this->assertNotNull( $rev1->getNext() );
                $dbw = wfGetDB( DB_MASTER );
                $rev = Revision::newNullRevision( $dbw, $page->getId(), 'a null revision', false );
  
 -              $this->assertNotEquals( $orig->getId(), $rev->getId(), 'new null revision shold have a different id from the original revision' );
 -              $this->assertEquals( $orig->getTextId(), $rev->getTextId(), 'new null revision shold have the same text id as the original revision' );
 -              $this->assertEquals( 'some testing text', $rev->getText() );
 +              $this->assertNotEquals( $orig->getId(), $rev->getId(),
 +                                                              'new null revision shold have a different id from the original revision' );
 +              $this->assertEquals( $orig->getTextId(), $rev->getTextId(),
 +                                                              'new null revision shold have the same text id as the original revision' );
 +              $this->assertEquals( 'some testing text', $rev->getContent()->getNativeData() );
        }
+       public function dataUserWasLastToEdit() {
+               return array(
+                       array( #0
+                               3, true, # actually the last edit
+                       ),
+                       array( #1
+                               2, true, # not the current edit, but still by this user
+                       ),
+                       array( #2
+                               1, false, # edit by another user
+                       ),
+                       array( #3
+                               0, false, # first edit, by this user, but another user edited in the mean time
+                       ),
+               );
+       }
+       /**
+        * @dataProvider dataUserWasLastToEdit
+        */
+       public function testUserWasLastToEdit( $sinceIdx, $expectedLast ) {
+               $userA = \User::newFromName( "RevisionStorageTest_userA" );
+               $userB = \User::newFromName( "RevisionStorageTest_userB" );
+               if ( $userA->getId() === 0 ) {
+                       $userA = \User::createNew( $userA->getName() );
+               }
+               if ( $userB->getId() === 0 ) {
+                       $userB = \User::createNew( $userB->getName() );
+               }
+               $dbw = wfGetDB( DB_MASTER );
+               $revisions = array();
+               // create revisions -----------------------------
+               $page = WikiPage::factory( Title::newFromText( 'RevisionStorageTest_testUserWasLastToEdit' ) );
+               # zero
+               $revisions[0] = new Revision( array(
+                       'page' => $page->getId(),
++                      'title' => $page->getTitle(), // we need the title to determine the page's default content model
+                       'timestamp' => '20120101000000',
+                       'user' => $userA->getId(),
+                       'text' => 'zero',
+                       'summary' => 'edit zero'
+               ) );
+               $revisions[0]->insertOn( $dbw );
+               # one
+               $revisions[1] = new Revision( array(
+                       'page' => $page->getId(),
++                      'title' => $page->getTitle(), // still need the title, because $page->getId() is 0 (there's no entry in the page table)
+                       'timestamp' => '20120101000100',
+                       'user' => $userA->getId(),
+                       'text' => 'one',
+                       'summary' => 'edit one'
+               ) );
+               $revisions[1]->insertOn( $dbw );
+               # two
+               $revisions[2] = new Revision( array(
+                       'page' => $page->getId(),
++                      'title' => $page->getTitle(),
+                       'timestamp' => '20120101000200',
+                       'user' => $userB->getId(),
+                       'text' => 'two',
+                       'summary' => 'edit two'
+               ) );
+               $revisions[2]->insertOn( $dbw );
+               # three
+               $revisions[3] = new Revision( array(
+                       'page' => $page->getId(),
++                      'title' => $page->getTitle(),
+                       'timestamp' => '20120101000300',
+                       'user' => $userA->getId(),
+                       'text' => 'three',
+                       'summary' => 'edit three'
+               ) );
+               $revisions[3]->insertOn( $dbw );
+               # four
+               $revisions[4] = new Revision( array(
+                       'page' => $page->getId(),
++                      'title' => $page->getTitle(),
+                       'timestamp' => '20120101000200',
+                       'user' => $userA->getId(),
+                       'text' => 'zero',
+                       'summary' => 'edit four'
+               ) );
+               $revisions[4]->insertOn( $dbw );
+               // test it ---------------------------------
+               $since = $revisions[ $sinceIdx ]->getTimestamp();
+               $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since );
+               $this->assertEquals( $expectedLast, $wasLast );
+       }
  }
diff --combined thumb.php
+++ b/thumb.php
@@@ -71,9 -71,10 +71,9 @@@ function wfThumbHandle404() 
        }
        # Just get the URI path (REDIRECT_URL/REQUEST_URI is either a full URL or a path)
        if ( substr( $uriPath, 0, 1 ) !== '/' ) {
 -              $bits = wfParseUrl( $uriPath );
 -              if ( $bits && isset( $bits['path'] ) ) {
 -                      $uriPath = $bits['path'];
 -              } else {
 +              $uri = new Uri( $uriPath );
 +              $uriPath = $uri->getPath();
 +              if ( $uriPath === null ) {
                        wfThumbError( 404, 'The source file for the specified thumbnail does not exist.' );
                        return;
                }
@@@ -95,6 -96,7 +95,7 @@@
   * @return void
   */
  function wfStreamThumb( array $params ) {
+       global $wgVaryOnXFP;
        wfProfileIn( __METHOD__ );
  
        $headers = array(); // HTTP headers to send
        }
  
        // Check permissions if there are read restrictions
+       $varyHeader = array();
        if ( !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true ) ) {
                if ( !$img->getTitle() || !$img->getTitle()->userCan( 'read' ) ) {
                        wfThumbError( 403, 'Access denied. You do not have permission to access ' .
                        return;
                }
                $headers[] = 'Cache-Control: private';
-               $headers[] = 'Vary: Cookie';
+               $varyHeader[] = 'Cookie';
        }
  
        // Check the source file storage path
  
        // Stream the file if it exists already...
        try {
+               $thumbName2 = $img->thumbName( $params, File::THUMB_FULL_NAME ); // b/c; "long" style
                // For 404 handled thumbnails, we only use the the base name of the URI
                // for the thumb params and the parent directory for the source file name.
                // Check that the zone relative path matches up so squid caches won't pick
                // up thumbs that would not be purged on source file deletion (bug 34231).
-               if ( isset( $params['rel404'] ) // thumbnail was handled via 404
-                       && urldecode( $params['rel404'] ) !== $img->getThumbRel( $thumbName ) )
-               {
-                       wfThumbError( 404, 'The source file for the specified thumbnail does not exist.' );
-                       wfProfileOut( __METHOD__ );
-                       return;
+               if ( isset( $params['rel404'] ) ) { // thumbnail was handled via 404
+                       if ( urldecode( $params['rel404'] ) === $img->getThumbRel( $thumbName ) ) {
+                               // Request for the canonical thumbnail name
+                       } elseif ( urldecode( $params['rel404'] ) === $img->getThumbRel( $thumbName2 ) ) {
+                               // Request for the "long" thumbnail name; redirect to canonical name
+                               $response = RequestContext::getMain()->getRequest()->response();
+                               $response->header( "HTTP/1.1 301 " . HttpStatus::getMessage( 301 ) );
+                               $response->header( 'Location: ' . wfExpandUrl( $img->getThumbUrl( $thumbName ), PROTO_CURRENT ) );
+                               $response->header( 'Expires: ' .
+                                       gmdate( 'D, d M Y H:i:s', time() + 7*86400 ) . ' GMT' );
+                               if ( $wgVaryOnXFP ) {
+                                       $varyHeader[] = 'X-Forwarded-Proto';
+                               }
+                               $response->header( 'Vary: ' . implode( ', ', $varyHeader ) );
+                               wfProfileOut( __METHOD__ );
+                               return;
+                       } else {
+                               wfThumbError( 404, 'The source file for the specified thumbnail does not exist.' );
+                               wfProfileOut( __METHOD__ );
+                               return;
+                       }
                }
                $thumbPath = $img->getThumbPath( $thumbName );
                if ( $img->getRepo()->fileExists( $thumbPath ) ) {
+                       $headers[] = 'Vary: ' . implode( ', ', $varyHeader );
                        $img->getRepo()->streamFile( $thumbPath, $headers );
                        wfProfileOut( __METHOD__ );
                        return;
                wfProfileOut( __METHOD__ );
                return;
        }
+       $headers[] = 'Vary: ' . implode( ', ', $varyHeader );
  
        // Thumbnail isn't already there, so create the new thumbnail...
        try {