merged from master
authordaniel <daniel.kinzler@wikimedia.de>
Wed, 25 Apr 2012 17:49:09 +0000 (19:49 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Wed, 25 Apr 2012 17:49:09 +0000 (19:49 +0200)
1  2 
.gitreview
includes/AutoLoader.php
includes/DefaultSettings.php
includes/EditPage.php
includes/Title.php
includes/api/ApiDelete.php
includes/api/ApiMain.php
languages/messages/MessagesEn.php
maintenance/refreshLinks.php

diff --combined .gitreview
@@@ -2,4 -2,5 +2,5 @@@
  host=gerrit.wikimedia.org
  port=29418
  project=mediawiki/core.git
- defaultbranch=Wikidata
+ defaultbranch=master
 -defaultrebase=0
++defaultrebase=0
diff --combined includes/AutoLoader.php
@@@ -27,6 -27,7 +27,7 @@@ $wgAutoloadLocalClasses = array
        'BadTitleError' => 'includes/Exception.php',
        'BaseTemplate' => 'includes/SkinTemplate.php',
        'Block' => 'includes/Block.php',
+       'CacheHelper' => 'includes/CacheHelper.php',
        'Category' => 'includes/Category.php',
        'Categoryfinder' => 'includes/Categoryfinder.php',
        'CategoryPage' => 'includes/CategoryPage.php',
@@@ -51,8 -52,6 +52,6 @@@
        'CookieJar' => 'includes/Cookie.php',
        'MWCryptRand' => 'includes/CryptRand.php',
        'CurlHttpRequest' => 'includes/HttpFunctions.php',
- //    'DBDataObject' => 'includes/DBDataObject.php',
- //    'DBTable' => 'includes/DBTable.php',
        'DeferrableUpdate' => 'includes/DeferredUpdates.php',
        'DeferredUpdates' => 'includes/DeferredUpdates.php',
        'DeprecatedGlobal' => 'includes/DeprecatedGlobal.php',
        'Http' => 'includes/HttpFunctions.php',
        'HttpError' => 'includes/Exception.php',
        'HttpRequest' => 'includes/HttpFunctions.old.php',
+       'ICacheHelper' => 'includes/CacheHelper.php',
        'IcuCollation' => 'includes/Collation.php',
        'IdentityCollation' => 'includes/Collation.php',
        'ImageGallery' => 'includes/ImageGallery.php',
        'RevisionList' => 'includes/RevisionList.php',
        'RSSFeed' => 'includes/Feed.php',
        'Sanitizer' => 'includes/Sanitizer.php',
 +    'SecondaryDataUpdate' => 'includes/SecondaryDataUpdate.php',
 +    'SecondaryDBDataUpdate' => 'includes/SecondaryDBDataUpdate.php',
        'ScopedPHPTimeout' => 'includes/ScopedPHPTimeout.php',
        'SiteConfiguration' => 'includes/SiteConfiguration.php',
        'SiteStats' => 'includes/SiteStats.php',
        'ZhClient' => 'includes/ZhClient.php',
        'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php',
  
 +    # content handler
 +    'Content' => 'includes/Content.php',
 +    'ContentHandler' => 'includes/ContentHandler.php',
 +    'CssContent' => 'includes/Content.php',
 +    'CssContentHandler' => 'includes/ContentHandler.php',
 +    'JavaScriptContent' => 'includes/Content.php',
 +    'JavaScriptContentHandler' => 'includes/ContentHandler.php',
 +    'MessageContent' => 'includes/Content.php',
 +    'TextContent' => 'includes/Content.php',
 +    'WikitextContent' => 'includes/Content.php',
 +    'WikitextContentHandler' => 'includes/ContentHandler.php',
 +
        # includes/actions
+       'CachedAction' => 'includes/actions/CachedAction.php',
        'CreditsAction' => 'includes/actions/CreditsAction.php',
        'DeleteAction' => 'includes/actions/DeleteAction.php',
        'EditAction' => 'includes/actions/EditAction.php',
        'ApiFormatDump' => 'includes/api/ApiFormatDump.php',
        'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php',
        'ApiFormatJson' => 'includes/api/ApiFormatJson.php',
 +      'ApiFormatNone' => 'includes/api/ApiFormatNone.php',
        'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php',
        'ApiFormatRaw' => 'includes/api/ApiFormatRaw.php',
        'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php',
        'ApiPurge' => 'includes/api/ApiPurge.php',
        'ApiQuery' => 'includes/api/ApiQuery.php',
        'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php',
-       'ApiQueryAllimages' => 'includes/api/ApiQueryAllimages.php',
+       'ApiQueryAllImages' => 'includes/api/ApiQueryAllImages.php',
        'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php',
-       'ApiQueryAllmessages' => 'includes/api/ApiQueryAllmessages.php',
-       'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php',
+       'ApiQueryAllMessages' => 'includes/api/ApiQueryAllMessages.php',
+       'ApiQueryAllPages' => 'includes/api/ApiQueryAllPages.php',
        'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php',
        'ApiQueryBacklinks' => 'includes/api/ApiQueryBacklinks.php',
        'ApiQueryBase' => 'includes/api/ApiQueryBase.php',
        'MySQLMasterPos' => 'includes/db/DatabaseMysql.php',
        'ORAField' => 'includes/db/DatabaseOracle.php',
        'ORAResult' => 'includes/db/DatabaseOracle.php',
+       'ORMResult' => 'includes/db/ORMResult.php',
+       'ORMRow' => 'includes/db/ORMRow.php',
+       'ORMTable' => 'includes/db/ORMTable.php',
        'PostgresField' => 'includes/db/DatabasePostgres.php',
        'ResultWrapper' => 'includes/db/DatabaseUtility.php',
        'SQLiteField' => 'includes/db/DatabaseSqlite.php',
        'SpecialBlockList' => 'includes/specials/SpecialBlockList.php',
        'SpecialBlockme' => 'includes/specials/SpecialBlockme.php',
        'SpecialBookSources' => 'includes/specials/SpecialBooksources.php',
+       'SpecialCachedPage' => 'includes/specials/SpecialCachedPage.php',
        'SpecialCategories' => 'includes/specials/SpecialCategories.php',
        'SpecialChangeEmail' => 'includes/specials/SpecialChangeEmail.php',
        'SpecialChangePassword' => 'includes/specials/SpecialChangePassword.php',
        'TestFileIterator' => 'tests/testHelpers.inc',
        'TestRecorder' => 'tests/testHelpers.inc',
  
 +      # tests/phpunit
 +      'WikitextContentTest' => 'tests/phpunit/includes/WikitextContentTest.php',
 +      'JavascriptContentTest' => 'tests/phpunit/includes/JavascriptContentTest.php',
 +      'DummyContentHandlerForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php',
 +      'DummyContentForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php',
 +
        # tests/parser
        'ParserTest' => 'tests/parser/parserTest.inc',
        'ParserTestParserHook' => 'tests/parser/parserTestsParserHook.php',
@@@ -640,17 -640,6 +640,17 @@@ $wgMediaHandlers = array
        'image/x-djvu' => 'DjVuHandler', // compat
  );
  
 +/**
 + * Plugins for page content model handling.
 + * Each entry in the array maps a model name type to a class name
 + */
 +$wgContentHandlers = array(
 +    CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', // the usual case
 +    CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', // dumb version, no syntax highlighting
 +    CONTENT_MODEL_CSS => 'CssContentHandler', // dumb version, no syntax highlighting
 +    CONTENT_MODEL_TEXT => 'TextContentHandler', // dumb plain text in <pre>
 +);
 +
  /**
   * Resizing can be done using PHP's internal image libraries or using
   * ImageMagick or another third-party converter, e.g. GraphicMagick.
@@@ -952,8 -941,7 +952,7 @@@ $wgImageLimits = array
        array( 640, 480 ),
        array( 800, 600 ),
        array( 1024, 768 ),
-       array( 1280, 1024 ),
-       array( 10000, 10000 )
+       array( 1280, 1024 )
  );
  
  /**
@@@ -5818,22 -5806,6 +5817,22 @@@ $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';
 +
  /**
   * For really cool vim folding this needs to be at the end:
   * vim: foldmarker=@{,@} foldmethod=marker
diff --combined includes/EditPage.php
@@@ -144,11 -144,11 +144,16 @@@ 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.
+        */
+       const EDITFORM_ID                  = 'editform';
        /**
         * @var Article
         */
        var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false;
        var $edittime = '', $section = '', $sectiontitle = '', $starttime = '';
        var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true;
 +      var $content_model = null, $content_format = null;
  
        # Placeholders for text injection by hooks (must be HTML)
        # extensions should take care to _append_ to the present value
        public $editFormTextBottom = '';
        public $editFormTextAfterContent = '';
        public $previewTextAfterContent = '';
 -      public $mPreloadText = '';
 +      public $mPreloadContent = null;
  
        /* $didSave should be set to true whenever an article was succesfully altered. */
        public $didSave = false;
        public function __construct( Article $article ) {
                $this->mArticle = $article;
                $this->mTitle = $article->getTitle();
 +
 +              $this->content_model = $this->mTitle->getContentModelName();
 +
 +              $handler = ContentHandler::getForModelName( $this->content_model );
 +              $this->content_format = $handler->getDefaultFormat(); #NOTE: should be overridden by format of actual revision
        }
  
        /**
                        return;
                }
  
 -              $content = $this->getContent();
 +              $content = $this->getContentObject();
  
                # Use the normal message if there's nothing to display
 -              if ( $this->firsttime && $content === '' ) {
 +              if ( $this->firsttime && $content->isEmpty() ) {
                        $action = $this->mTitle->exists() ? 'edit' :
                                ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
                        throw new PermissionsError( $action, $permErrors );
                # If the user made changes, preserve them when showing the markup
                # (This happens when a user is blocked during edit, for instance)
                if ( !$this->firsttime ) {
 -                      $content = $this->textbox1;
 +                      $text = $this->textbox1;
                        $wgOut->addWikiMsg( 'viewyourtext' );
                } else {
 +                      $text = $content->serialize( $this->content_format );
                        $wgOut->addWikiMsg( 'viewsourcetext' );
                }
  
 -              $this->showTextbox( $content, 'wpTextbox1', array( 'readonly' ) );
 +              $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) );
  
                $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ),
                        Linker::formatTemplates( $this->getTemplates() ) ) );
                } else {
                        # Not a posted form? Start with nothing.
                        wfDebug( __METHOD__ . ": Not a posted form.\n" );
 -                      $this->textbox1     = '';
 +                      $this->textbox1     = ''; #FIXME: track content object
                        $this->summary      = '';
                        $this->sectiontitle = '';
                        $this->edittime     = '';
                        }
                }
  
 +              $this->oldid = $request->getInt( 'oldid' );
 +
                $this->bot = $request->getBool( 'bot', true );
                $this->nosummary = $request->getBool( 'nosummary' );
  
 -              $this->oldid = $request->getInt( 'oldid' );
 +              $content_handler = ContentHandler::getForTitle( $this->mTitle );
 +              $this->content_model = $request->getText( 'model', $content_handler->getModelName() ); #may be overridden by revision
 +              $this->content_format = $request->getText( 'format', $content_handler->getDefaultFormat() ); #may be overridden by revision
 +
 +              #TODO: check if the desired model is allowed in this namespace, and if a transition from the page's current model to the new model is allowed
 +              #TODO: check if the desired content model supports the given content format!
  
                $this->live = $request->getCheck( 'live' );
                $this->editintro = $request->getText( 'editintro',
        function initialiseForm() {
                global $wgUser;
                $this->edittime = $this->mArticle->getTimestamp();
 -              $this->textbox1 = $this->getContent( false );
 +
 +              $content = $this->getContentObject( false ); #TODO: track content object?!
 +              $this->textbox1 = $content->serialize( $this->content_format );
 +
                // activate checkboxes if user wants them to be always active
                # Sort out the "watch" checkbox
                if ( $wgUser->getOption( 'watchdefault' ) ) {
         * @param $def_text string
         * @return mixed string on success, $def_text for invalid sections
         * @private
 +       * @deprecated since 1.20
         */
 -      function getContent( $def_text = '' ) {
 -              global $wgOut, $wgRequest, $wgParser;
 +      function getContent( $def_text = false ) { #FIXME: deprecated, replace usage!
 +              if ( $def_text !== null && $def_text !== false && $def_text !== '' ) {
 +                      $def_content = ContentHandler::makeContent( $def_text, $this->getTitle() );
 +              } else {
 +                      $def_content = false;
 +              }
 +
 +              $content = $this->getContentObject( $def_content );
 +
 +              return $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?
 +      }
 +
 +      private function getContentObject( $def_content = null ) { #FIXME: use this!
 +              global $wgOut, $wgRequest;
  
                wfProfileIn( __METHOD__ );
  
 -              $text = false;
 +              $content = false;
  
                // For message page not locally set, use the i18n message.
                // For other non-existent articles, use preload text if any.
                if ( !$this->mTitle->exists() || $this->section == 'new' ) {
                        if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
                                # If this is a system message, get the default text.
 -                              $text = $this->mTitle->getDefaultMessageText();
 +                              $msg = $this->mTitle->getDefaultMessageText();
 +
 +                              $content = ContentHandler::makeContent( $msg, $this->mTitle );
                        }
 -                      if ( $text === false ) {
 +                      if ( $content === false ) {
                                # If requested, preload some text.
                                $preload = $wgRequest->getVal( 'preload',
                                        // Custom preload text for new sections
                                        $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
 -                              $text = $this->getPreloadedText( $preload );
 +
 +                              $content = $this->getPreloadedContent( $preload );
                        }
                // For existing pages, get text based on "undo" or section parameters.
                } else {
                        if ( $this->section != '' ) {
                                // Get section edit text (returns $def_text for invalid sections)
 -                              $text = $wgParser->getSection( $this->getOriginalContent(), $this->section, $def_text );
 +                              $orig = $this->getOriginalContent();
 +                              $content = $orig ? $orig->getSection( $this->section ) : null;
 +
 +                              if ( !$content ) $content = $def_content;
                        } else {
                                $undoafter = $wgRequest->getInt( 'undoafter' );
                                $undo = $wgRequest->getInt( 'undo' );
  
                                        # Sanity check, make sure it's the right page,
                                        # the revisions exist and they were not deleted.
 -                                      # Otherwise, $text will be left as-is.
 +                                      # Otherwise, $content will be left as-is.
                                        if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
                                                $undorev->getPage() == $oldrev->getPage() &&
                                                $undorev->getPage() == $this->mTitle->getArticleID() &&
                                                !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
                                                !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
  
 -                                              $text = $this->mArticle->getUndoText( $undorev, $oldrev );
 -                                              if ( $text === false ) {
 +                        $content = $this->mArticle->getUndoContent( $undorev, $oldrev );
 +
 +                                              if ( $content === false ) {
                                                        # Warn the user that something went wrong
                                                        $undoMsg = 'failure';
                                                } else {
                                                wfMsgNoTrans( 'undo-' . $undoMsg ) . '</div>', true, /* interface */true );
                                }
  
 -                              if ( $text === false ) {
 -                                      $text = $this->getOriginalContent();
 +                              if ( $content === false ) {
 +                                      $content = $this->getOriginalContent();
                                }
                        }
                }
  
                wfProfileOut( __METHOD__ );
 -              return $text;
 +              return $content;
        }
  
        /**
         */
        private function getOriginalContent() {
                if ( $this->section == 'new' ) {
 -                      return $this->getCurrentText();
 +                      return $this->getCurrentContent();
                }
                $revision = $this->mArticle->getRevisionFetched();
                if ( $revision === null ) {
 -                      return '';
 -              }
 -              return $this->mArticle->getContent();
 -      }
 +                      if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModelName();
 +                      $handler = ContentHandler::getForModelName( $this->content_model );
  
 -      /**
 -       * 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.
 -       *
 -       * @since 1.19
 -       * @return string
 -       */
 -      private function getCurrentText() {
 -              $text = $this->mArticle->getRawText();
 -              if ( $text === false ) {
 -                      return '';
 -              } else {
 -                      return $text;
 +                      return $handler->makeEmptyContent();
                }
 +        $content = $revision->getContent();
 +              return $content;
        }
  
 -      /**
 -       * Use this method before edit() to preload some text into the edit box
 -       *
 -       * @param $text string
 -       */
 -      public function setPreloadedText( $text ) {
 -              $this->mPreloadText = $text;
 -      }
 -
 -      /**
 -       * Get the contents to be preloaded into the box, either set by
 -       * an earlier setPreloadText() or by loading the given page.
 -       *
 -       * @param $preload String: representing the title to preload from.
 -       * @return String
 -       */
 -      protected function getPreloadedText( $preload ) {
 -              global $wgUser, $wgParser;
 -
 -              if ( !empty( $this->mPreloadText ) ) {
 -                      return $this->mPreloadText;
 -              }
 -
 -              if ( $preload === '' ) {
 -                      return '';
 -              }
 -
 -              $title = Title::newFromText( $preload );
 -              # Check for existence to avoid getting MediaWiki:Noarticletext
 -              if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
 -                      return '';
 -              }
 -
 -              $page = WikiPage::factory( $title );
 -              if ( $page->isRedirect() ) {
 -                      $title = $page->getRedirectTarget();
 -                      # Same as before
 -                      if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
 -                              return '';
 -                      }
 -                      $page = WikiPage::factory( $title );
 -              }
 -
 -              $parserOptions = ParserOptions::newFromUser( $wgUser );
 -              return $wgParser->getPreloadText( $page->getRawText(), $title, $parserOptions );
 -      }
 +    /**
 +     * 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.20
 +     * @return string
 +     */
 +    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()->getContentModelName();
 +            $handler = ContentHandler::getForModelName( $this->content_model );
 +
 +            return $handler->makeEmptyContent();
 +        } else {
 +            #FIXME: nasty side-effect!
 +            $this->content_model = $rev->getContentModelName();
 +            $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.20
 +     */
 +    public function setPreloadedText( $text ) { #FIXME: deprecated, use setPreloadedContent()
 +        wfDeprecated( __METHOD__, "1.20" );
 +
 +        $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +
 +        $this->setPreloadedContent( $content );
 +    }
 +
 +    /**
 +     * Use this method before edit() to preload some content into the edit box
 +     *
 +     * @param $content Content
 +     */
 +    public function setPreloadedContent( Content $content ) { #FIXME: use this!
 +        $this->mPreloadedContent = $content;
 +    }
 +
 +    /**
 +     * 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 String
 +     * @deprecated since 1.20
 +     */
 +    protected function getPreloadedText( $preload ) { #FIXME: B/C only, replace usage!
 +        wfDeprecated( __METHOD__, "1.20" );
 +
 +        $content = $this->getPreloadedContent( $preload );
 +        $text = $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?!
 +
 +        return $text;
 +    }
 +
 +    protected function getPreloadedContent( $preload ) { #FIXME: use this!
 +        global $wgUser;
 +
 +        if ( !empty( $this->mPreloadContent ) ) {
 +            return $this->mPreloadContent;
 +        }
 +
 +        $handler = ContentHandler::getForTitle( $this->getTitle() );
 +
 +        if ( $preload === '' ) {
 +            return $handler->makeEmptyContent();
 +        }
 +
 +        $title = Title::newFromText( $preload );
 +        # Check for existence to avoid getting MediaWiki:Noarticletext
 +        if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
 +            return $handler->makeEmptyContent();
 +        }
 +
 +        $page = WikiPage::factory( $title );
 +        if ( $page->isRedirect() ) {
 +            $title = $page->getRedirectTarget();
 +            # Same as before
 +            if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
 +                return $handler->makeEmptyContent();
 +            }
 +            $page = WikiPage::factory( $title );
 +        }
 +
 +        $parserOptions = ParserOptions::newFromUser( $wgUser );
 +        $content = $page->getContent( Revision::RAW );
 +
 +        return $content->preloadTransform( $title, $parserOptions );
 +    }
  
        /**
         * Make sure the form isn't faking a user's credentials.
                        case self::AS_FILTERING:
                                return false;
  
 +                      case self::AS_PARSE_ERROR:
 +                              $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>');
 +                              #FIXME: cause editform to be shown again, not just an error!
 +                              return false;
 +
                        case self::AS_SUCCESS_NEW_ARTICLE:
                                $query = $resultDetails['redirect'] ? 'redirect=no' : '';
                                $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
  
                # Check image redirect
                if ( $this->mTitle->getNamespace() == NS_FILE &&
 -                      Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
 +                      Title::newFromRedirect( $this->textbox1 ) instanceof Title && #FIXME: use content handler to check for redirect
                        !$wgUser->isAllowed( 'upload' ) ) {
                                $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
                                $status->setResult( false, $code );
                $aid = $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE );
                $new = ( $aid == 0 );
  
 -              if ( $new ) {
 -                      // Late check for create permission, just in case *PARANOIA*
 -                      if ( !$this->mTitle->userCan( 'create' ) ) {
 -                              $status->fatal( 'nocreatetext' );
 -                              $status->value = self::AS_NO_CREATE_PERMISSION;
 -                              wfDebug( __METHOD__ . ": no create permission\n" );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 -
 -                      # Don't save a new article if it's blank.
 -                      if ( $this->textbox1 == '' ) {
 -                              $status->setResult( false, self::AS_BLANK_ARTICLE );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 -
 -                      // Run post-section-merge edit filter
 -                      if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
 -                              # Error messages etc. could be handled within the hook...
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      } elseif ( $this->hookError != '' ) {
 -                              # ...or the hook could be expecting us to produce an error
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR_EXPECTED;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 -
 -                      $text = $this->textbox1;
 -                      $result['sectionanchor'] = '';
 -                      if ( $this->section == 'new' ) {
 -                              if ( $this->sectiontitle !== '' ) {
 -                                      // Insert the section title above the content.
 -                                      $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->sectiontitle ) . "\n\n" . $text;
 -
 -                                      // Jump to the new section
 -                                      $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 -
 -                                      // If no edit summary was specified, create one automatically from the section
 -                                      // title and have it link to the new section. Otherwise, respect the summary as
 -                                      // passed.
 -                                      if ( $this->summary === '' ) {
 -                                              $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 -                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 -                                      }
 -                              } elseif ( $this->summary !== '' ) {
 -                                      // Insert the section title above the content.
 -                                      $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $text;
 -
 -                                      // Jump to the new section
 -                                      $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 -
 -                                      // Create a link to the new section from the edit summary.
 -                                      $cleanSummary = $wgParser->stripSectionName( $this->summary );
 -                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 -                              }
 -                      }
 -
 -                      $status->value = self::AS_SUCCESS_NEW_ARTICLE;
 -
 -              } else {
 -
 -                      # 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 = true;
 -                              if ( $this->section == 'new' ) {
 -                                      if ( $this->mArticle->getUserText() == $wgUser->getName() &&
 -                                              $this->mArticle->getComment() == $this->summary ) {
 -                                              // Probably a duplicate submission of a new comment.
 -                                              // This can happen when squid resends a request after
 -                                              // a timeout but the first one actually went through.
 -                                              wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
 -                                      } else {
 -                                              // New comment; suppress conflict.
 -                                              $this->isConflict = false;
 -                                              wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
 -                                      }
 -                              } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) {
 -                                      # Suppress edit conflict with self, except for section edits where merging is required.
 -                                      wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
 -                                      $this->isConflict = false;
 -                              }
 -                      }
 -
 -                      // If sectiontitle is set, use it, otherwise use the summary as the section title (for
 -                      // backwards compatibility with old forms/bots).
 -                      if ( $this->sectiontitle !== '' ) {
 -                              $sectionTitle = $this->sectiontitle;
 -                      } else {
 -                              $sectionTitle = $this->summary;
 -                      }
 -
 -                      if ( $this->isConflict ) {
 -                              wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
 -                              $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle, $this->edittime );
 -                      } else {
 -                              wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
 -                              $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle );
 -                      }
 -                      if ( is_null( $text ) ) {
 -                              wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
 -                              $this->isConflict = true;
 -                              $text = $this->textbox1; // do not try to merge here!
 -                      } elseif ( $this->isConflict ) {
 -                              # Attempt merge
 -                              if ( $this->mergeChangesInto( $text ) ) {
 -                                      // Successful merge! Maybe we should tell the user the good news?
 -                                      $this->isConflict = false;
 -                                      wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
 -                              } else {
 -                                      $this->section = '';
 -                                      $this->textbox1 = $text;
 -                                      wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
 -                              }
 -                      }
 -
 -                      if ( $this->isConflict ) {
 -                              $status->setResult( false, self::AS_CONFLICT_DETECTED );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 -
 -                      // Run post-section-merge edit filter
 -                      if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) {
 -                              # Error messages etc. could be handled within the hook...
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      } elseif ( $this->hookError != '' ) {
 -                              # ...or the hook could be expecting us to produce an error
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR_EXPECTED;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 -
 -                      # Handle the user preference to force summaries here, but not for null edits
 -                      if ( $this->section != 'new' && !$this->allowBlankSummary
 -                              && $this->getOriginalContent() != $text
 -                              && !Title::newFromRedirect( $text ) ) # check if it's not a redirect
 -                      {
 -                              if ( md5( $this->summary ) == $this->autoSumm ) {
 -                                      $this->missingSummary = true;
 -                                      $status->fatal( 'missingsummary' );
 -                                      $status->value = self::AS_SUMMARY_NEEDED;
 -                                      wfProfileOut( __METHOD__ );
 -                                      return $status;
 -                              }
 -                      }
 -
 -                      # And a similar thing for new sections
 -                      if ( $this->section == 'new' && !$this->allowBlankSummary ) {
 -                              if ( trim( $this->summary ) == '' ) {
 -                                      $this->missingSummary = true;
 -                                      $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
 -                                      $status->value = self::AS_SUMMARY_NEEDED;
 -                                      wfProfileOut( __METHOD__ );
 -                                      return $status;
 -                              }
 -                      }
 -
 -                      # All's well
 -                      wfProfileIn( __METHOD__ . '-sectionanchor' );
 -                      $sectionanchor = '';
 -                      if ( $this->section == 'new' ) {
 -                              if ( $this->textbox1 == '' ) {
 -                                      $this->missingComment = true;
 -                                      $status->fatal( 'missingcommenttext' );
 -                                      $status->value = self::AS_TEXTBOX_EMPTY;
 -                                      wfProfileOut( __METHOD__ . '-sectionanchor' );
 -                                      wfProfileOut( __METHOD__ );
 -                                      return $status;
 -                              }
 -                              if ( $this->sectiontitle !== '' ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 -                                      // If no edit summary was specified, create one automatically from the section
 -                                      // title and have it link to the new section. Otherwise, respect the summary as
 -                                      // passed.
 -                                      if ( $this->summary === '' ) {
 -                                              $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 -                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 -                                      }
 -                              } elseif ( $this->summary !== '' ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 -                                      # This is a new section, so create a link to the new section
 -                                      # in the revision summary.
 -                                      $cleanSummary = $wgParser->stripSectionName( $this->summary );
 -                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 -                              }
 -                      } elseif ( $this->section != '' ) {
 -                              # Try to get a section anchor from the section source, redirect to edited section if header found
 -                              # XXX: might be better to integrate this into Article::replaceSection
 -                              # for duplicate heading checking and maybe parsing
 -                              $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
 -                              # we can't deal with anchors, includes, html etc in the header for now,
 -                              # headline would need to be parsed to improve this
 -                              if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
 -                              }
 -                      }
 -                      $result['sectionanchor'] = $sectionanchor;
 -                      wfProfileOut( __METHOD__ . '-sectionanchor' );
 -
 -                      // Save errors may fall down to the edit form, but we've now
 -                      // merged the section into full text. Clear the section field
 -                      // so that later submission of conflict forms won't try to
 -                      // replace that into a duplicated mess.
 -                      $this->textbox1 = $text;
 -                      $this->section = '';
 -
 -                      $status->value = self::AS_SUCCESS_UPDATE;
 -              }
 -
 -              // Check for length errors again now that the section is merged in
 -              $this->kblength = (int)( strlen( $text ) / 1024 );
 -              if ( $this->kblength > $wgMaxArticleSize ) {
 -                      $this->tooBig = true;
 -                      $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
 +              try {
 +            if ( $new ) {
 +                // Late check for create permission, just in case *PARANOIA*
 +                if ( !$this->mTitle->userCan( 'create' ) ) {
 +                    $status->fatal( 'nocreatetext' );
 +                    $status->value = self::AS_NO_CREATE_PERMISSION;
 +                    wfDebug( __METHOD__ . ": no create permission\n" );
 +                    wfProfileOut( __METHOD__ );
 +                    return $status;
 +                }
 +
 +                # Don't save a new article if it's blank.
 +                if ( $this->textbox1 == '' ) {
 +                    $status->setResult( false, self::AS_BLANK_ARTICLE );
 +                    wfProfileOut( __METHOD__ );
 +                    return $status;
 +                }
 +
 +                // Run post-section-merge edit filter
 +                if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
 +                    # Error messages etc. could be handled within the hook...
 +                    $status->fatal( 'hookaborted' );
 +                    $status->value = self::AS_HOOK_ERROR;
 +                    wfProfileOut( __METHOD__ );
 +                    return $status;
 +                } elseif ( $this->hookError != '' ) {
 +                    # ...or the hook could be expecting us to produce an error
 +                    $status->fatal( 'hookaborted' );
 +                    $status->value = self::AS_HOOK_ERROR_EXPECTED;
 +                    wfProfileOut( __METHOD__ );
 +                    return $status;
 +                }
 +
 +                $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 +
 +                $result['sectionanchor'] = '';
 +                if ( $this->section == 'new' ) {
 +                    if ( $this->sectiontitle !== '' ) {
 +                        // Insert the section title above the content.
 +                        $content = $content->addSectionHeader( $this->sectiontitle );
 +
 +                        // Jump to the new section
 +                        $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 +
 +                        // If no edit summary was specified, create one automatically from the section
 +                        // title and have it link to the new section. Otherwise, respect the summary as
 +                        // passed.
 +                        if ( $this->summary === '' ) {
 +                            $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 +                            $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 +                        }
 +                    } elseif ( $this->summary !== '' ) {
 +                        // Insert the section title above the content.
 +                        $content = $content->addSectionHeader( $this->sectiontitle );
 +
 +                        // Jump to the new section
 +                        $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 +
 +                        // Create a link to the new section from the edit summary.
 +                        $cleanSummary = $wgParser->stripSectionName( $this->summary );
 +                        $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 +                    }
 +                }
 +
 +                $status->value = self::AS_SUCCESS_NEW_ARTICLE;
 +
 +            } 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 = true;
 +                    if ( $this->section == 'new' ) {
 +                        if ( $this->mArticle->getUserText() == $wgUser->getName() &&
 +                            $this->mArticle->getComment() == $this->summary ) {
 +                            // Probably a duplicate submission of a new comment.
 +                            // This can happen when squid resends a request after
 +                            // a timeout but the first one actually went through.
 +                            wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
 +                        } else {
 +                            // New comment; suppress conflict.
 +                            $this->isConflict = false;
 +                            wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
 +                        }
 +                    } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) {
 +                        # Suppress edit conflict with self, except for section edits where merging is required.
 +                        wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
 +                        $this->isConflict = false;
 +                    }
 +                }
 +
 +                // If sectiontitle is set, use it, otherwise use the summary as the section title (for
 +                // backwards compatibility with old forms/bots).
 +                if ( $this->sectiontitle !== '' ) {
 +                    $sectionTitle = $this->sectiontitle;
 +                } else {
 +                    $sectionTitle = $this->summary;
 +                }
 +
 +                $textbox_content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 +                $content = null;
 +
 +                if ( $this->isConflict ) {
 +                    wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
 +                    $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle, $this->edittime );
 +                } else {
 +                    wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
 +                    $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle );
 +                }
 +
 +                if ( is_null( $content ) ) {
 +                    wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
 +                    $this->isConflict = true;
 +                    $content = $textbox_content; // do not try to merge here!
 +                } elseif ( $this->isConflict ) {
 +                    # Attempt merge
 +                    if ( $this->mergeChangesIntoContent( $textbox_content ) ) {
 +                        // Successful merge! Maybe we should tell the user the good news?
 +                        $this->isConflict = false;
 +                        $content = $textbox_content;
 +                        wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
 +                    } else {
 +                        $this->section = '';
 +                        #$this->textbox1 = $text; #redundant, nothing to do here?
 +                        wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
 +                    }
 +                }
 +
 +                if ( $this->isConflict ) {
 +                    $status->setResult( false, self::AS_CONFLICT_DETECTED );
 +                    wfProfileOut( __METHOD__ );
 +                    return $status;
 +                }
 +
 +                // Run post-section-merge edit filter
 +                if ( !wfRunHooks( 'EditFilterMerged', array( $this, $content->serialize( $this->content_format ), &$this->hookError, $this->summary ) )
 +                        || !wfRunHooks( 'EditFilterMergedContent', array( $this, $content, &$this->hookError, $this->summary ) ) ) { #FIXME: document new hook
 +                    # Error messages etc. could be handled within the hook...
 +                    $status->fatal( 'hookaborted' );
 +                    $status->value = self::AS_HOOK_ERROR;
 +                    wfProfileOut( __METHOD__ );
 +                    return $status;
 +                } elseif ( $this->hookError != '' ) {
 +                    # ...or the hook could be expecting us to produce an error
 +                    $status->fatal( 'hookaborted' );
 +                    $status->value = self::AS_HOOK_ERROR_EXPECTED;
 +                    wfProfileOut( __METHOD__ );
 +                    return $status;
 +                }
 +
 +                $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 +
 +                # Handle the user preference to force summaries here, but not for null edits
 +                if ( $this->section != 'new' && !$this->allowBlankSummary
 +                    && !$content->equals( $this->getOriginalContent() )
 +                    && !$content->isRedirect() ) # check if it's not a redirect
 +                {
 +                    if ( md5( $this->summary ) == $this->autoSumm ) {
 +                        $this->missingSummary = true;
 +                        $status->fatal( 'missingsummary' );
 +                        $status->value = self::AS_SUMMARY_NEEDED;
 +                        wfProfileOut( __METHOD__ );
 +                        return $status;
 +                    }
 +                }
 +
 +                # And a similar thing for new sections
 +                if ( $this->section == 'new' && !$this->allowBlankSummary ) {
 +                    if ( trim( $this->summary ) == '' ) {
 +                        $this->missingSummary = true;
 +                        $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
 +                        $status->value = self::AS_SUMMARY_NEEDED;
 +                        wfProfileOut( __METHOD__ );
 +                        return $status;
 +                    }
 +                }
 +
 +                # All's well
 +                wfProfileIn( __METHOD__ . '-sectionanchor' );
 +                $sectionanchor = '';
 +                if ( $this->section == 'new' ) {
 +                    if ( $this->textbox1 == '' ) {
 +                        $this->missingComment = true;
 +                        $status->fatal( 'missingcommenttext' );
 +                        $status->value = self::AS_TEXTBOX_EMPTY;
 +                        wfProfileOut( __METHOD__ . '-sectionanchor' );
 +                        wfProfileOut( __METHOD__ );
 +                        return $status;
 +                    }
 +                    if ( $this->sectiontitle !== '' ) {
 +                        $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 +                        // If no edit summary was specified, create one automatically from the section
 +                        // title and have it link to the new section. Otherwise, respect the summary as
 +                        // passed.
 +                        if ( $this->summary === '' ) {
 +                            $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 +                            $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 +                        }
 +                    } elseif ( $this->summary !== '' ) {
 +                        $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 +                        # This is a new section, so create a link to the new section
 +                        # in the revision summary.
 +                        $cleanSummary = $wgParser->stripSectionName( $this->summary );
 +                        $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 +                    }
 +                } elseif ( $this->section != '' ) {
 +                    # Try to get a section anchor from the section source, redirect to edited section if header found
 +                    # XXX: might be better to integrate this into Article::replaceSection
 +                    # for duplicate heading checking and maybe parsing
 +                    $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
 +                    # we can't deal with anchors, includes, html etc in the header for now,
 +                    # headline would need to be parsed to improve this
 +                    if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
 +                        $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
 +                    }
 +                }
 +                $result['sectionanchor'] = $sectionanchor;
 +                wfProfileOut( __METHOD__ . '-sectionanchor' );
 +
 +                // Save errors may fall down to the edit form, but we've now
 +                // merged the section into full text. Clear the section field
 +                // so that later submission of conflict forms won't try to
 +                // replace that into a duplicated mess.
 +                    $this->textbox1 = $content->serialize( $this->content_format );
 +                $this->section = '';
 +
 +                $status->value = self::AS_SUCCESS_UPDATE;
 +            }
 +
 +            // Check for length errors again now that the section is merged in
 +                $this->kblength = (int)( strlen( $content->serialize( $this->content_format ) ) / 1024 );
 +            if ( $this->kblength > $wgMaxArticleSize ) {
 +                $this->tooBig = true;
 +                $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
 +                wfProfileOut( __METHOD__ );
 +                return $status;
 +            }
 +
 +            $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
 +                ( $new ? EDIT_NEW : EDIT_UPDATE ) |
 +                ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
 +                ( $bot ? EDIT_FORCE_BOT : 0 );
 +
 +                $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags, false, null, $this->content_format );
 +
 +            if ( $doEditStatus->isOK() ) {
 +                    $result['redirect'] = $content->isRedirect();
 +                $this->commitWatch();
 +                wfProfileOut( __METHOD__ );
 +                return $status;
 +            } else {
 +                $this->isConflict = true;
 +                $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares
 +                wfProfileOut( __METHOD__ );
 +                return $doEditStatus;
 +            }
 +              } catch (MWContentSerializationException $ex) {
 +                      $status->fatal( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $status->value = self::AS_PARSE_ERROR;
                        wfProfileOut( __METHOD__ );
                        return $status;
                }
 -
 -              $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
 -                      ( $new ? EDIT_NEW : EDIT_UPDATE ) |
 -                      ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
 -                      ( $bot ? EDIT_FORCE_BOT : 0 );
 -
 -              $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags );
 -
 -              if ( $doEditStatus->isOK() ) {
 -                      $result['redirect'] = Title::newFromRedirect( $text ) !== null;
 -                      $this->commitWatch();
 -                      wfProfileOut( __METHOD__ );
 -                      return $status;
 -              } else {
 -                      $this->isConflict = true;
 -                      $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares
 -                      wfProfileOut( __METHOD__ );
 -                      return $doEditStatus;
 -              }
        }
  
        /**
                return true;
        }
  
 -      /**
 -       * @private
 -       * @todo document
 -       *
 -       * @parma $editText string
 -       *
 -       * @return bool
 -       */
 -      function mergeChangesInto( &$editText ) {
 -              wfProfileIn( __METHOD__ );
 -
 -              $db = wfGetDB( DB_MASTER );
 -
 -              // This is the revision the editor started from
 -              $baseRevision = $this->getBaseRevision();
 -              if ( is_null( $baseRevision ) ) {
 -                      wfProfileOut( __METHOD__ );
 -                      return false;
 -              }
 -              $baseText = $baseRevision->getText();
 -
 -              // The current state, we want to merge updates into it
 -              $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
 -              if ( is_null( $currentRevision ) ) {
 -                      wfProfileOut( __METHOD__ );
 -                      return false;
 -              }
 -              $currentText = $currentRevision->getText();
 -
 -              $result = '';
 -              if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
 -                      $editText = $result;
 -                      wfProfileOut( __METHOD__ );
 -                      return true;
 -              } else {
 -                      wfProfileOut( __METHOD__ );
 -                      return false;
 -              }
 -      }
 +    /**
 +     * @private
 +     * @todo document
 +     *
 +     * @parma $editText string
 +     *
 +     * @return bool
 +     * @deprecated since 1.20
 +     */
 +    function mergeChangesInto( &$editText ){
 +        wfDebug( __METHOD__, "1.20" );
 +
 +        $editContent = ContentHandler::makeContent( $editText, $this->getTitle(), $this->content_model, $this->content_format );
 +
 +        $ok = $this->mergeChangesIntoContent( $editContent );
 +
 +        if ( $ok ) {
 +            $editText = $editContent->serialize( $this->content_format ); #XXX: really serialize?!
 +            return true;
 +        } else {
 +            return false;
 +        }
 +    }
 +
 +    /**
 +     * @private
 +     * @todo document
 +     *
 +     * @parma $editText string
 +     *
 +     * @return bool
 +     * @since since 1.20
 +     */
 +    private function mergeChangesIntoContent( &$editContent ){
 +        wfProfileIn( __METHOD__ );
 +
 +        $db = wfGetDB( DB_MASTER );
 +
 +        // This is the revision the editor started from
 +        $baseRevision = $this->getBaseRevision();
 +        if ( is_null( $baseRevision ) ) {
 +            wfProfileOut( __METHOD__ );
 +            return false;
 +        }
 +        $baseContent = $baseRevision->getContent();
 +
 +        // The current state, we want to merge updates into it
 +        $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
 +        if ( is_null( $currentRevision ) ) {
 +            wfProfileOut( __METHOD__ );
 +            return false;
 +        }
 +        $currentContent = $currentRevision->getContent();
 +
 +        $handler = ContentHandler::getForModelName( $baseContent->getModelName() );
 +
 +        $result = $handler->merge3( $baseContent, $editContent, $currentContent );
 +
 +        if ( $result ) {
 +            $editContent = $result;
 +            wfProfileOut( __METHOD__ );
 +            return true;
 +        } else {
 +            wfProfileOut( __METHOD__ );
 +            return false;
 +        }
 +    }
  
        /**
         * @return Revision
                        }
                }
  
-               $wgOut->addHTML( Html::openElement( 'form', array( 'id' => 'editform', 'name' => 'editform',
 +              #FIXME: add EditForm plugin interface and use it here! #FIXME: search for textarea1 and textares2, and allow EditForm to override all uses.
+               $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID,
                        'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ),
                        'enctype' => 'multipart/form-data' ) ) );
  
  
                $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
  
 +              $wgOut->addHTML( Html::hidden( 'format', $this->content_format ) );
 +              $wgOut->addHTML( Html::hidden( 'model', $this->content_model ) );
 +
                if ( $this->section == 'new' ) {
                        $this->showSummaryInput( true, $this->summary );
                        $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
                        // resolved between page source edits and custom ui edits using the
                        // custom edit ui.
                        $this->textbox2 = $this->textbox1;
 -                      $this->textbox1 = $this->getCurrentText();
 +
 +                      $content = $this->getCurrentContent();
 +                      $this->textbox1 = $content->serialize( $this->content_format );
  
                        $this->showTextbox1();
                } else {
                $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6, 'readonly' ) );
        }
  
 -      protected function showTextbox( $content, $name, $customAttribs = array() ) {
 +      protected function showTextbox( $text, $name, $customAttribs = array() ) {
                global $wgOut, $wgUser;
  
 -              $wikitext = $this->safeUnicodeOutput( $content );
 +              $wikitext = $this->safeUnicodeOutput( $text );
                if ( strval( $wikitext ) !== '' ) {
                        // Ensure there's a newline at the end, otherwise adding lines
                        // is awkward.
        function showDiff() {
                global $wgUser, $wgContLang, $wgParser, $wgOut;
  
-         $oldContent = $this->getOriginalContent();
+               $oldtitlemsg = 'currentrev';
+               # if message does not exist, show diff against the preloaded default
+               if( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
+                       $oldtext = $this->mTitle->getDefaultMessageText();
+                       if( $oldtext !== false ) {
+                               $oldtitlemsg = 'defaultmessagetext';
++                              $oldContent = ContentHandler::makeContent( $oldtext, $this->mTitle );
++                      } else {
++                              $oldContent = null;
+                       }
+               } else {
 -                      $oldtext = $this->mArticle->getRawText();
++                      $oldContent = $this->getOriginalContent();
++              }
 +
 +        $textboxContent = ContentHandler::makeContent( $this->textbox1, $this->getTitle(),
 +                                                                                                              $this->content_model, $this->content_format ); #XXX: handle parse errors ?
 +
 +        $newContent = $this->mArticle->replaceSectionContent(
 +                                                    $this->section, $textboxContent,
 +                                                    $this->summary, $this->edittime );
 +
 +        # hanlde legacy text-based hook
 +        $newtext_orig = $newContent->serialize( $this->content_format );
 +        $newtext = $newtext_orig; #clone
 +        wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
 +
 +              if ( $newtext != $newtext_orig ) {
 +                              #if the hook changed the text, create a new Content object accordingly.
 +                              $newContent = ContentHandler::makeContent( $newtext, $this->getTitle(), $newContent->getModelName() ); #XXX: handle parse errors ?
                }
 -              $newtext = $this->mArticle->replaceSection(
 -                      $this->section, $this->textbox1, $this->summary, $this->edittime );
  
 -              wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
 +              wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) ); #FIXME: document new hook
  
                $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
 -              $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts );
 +        $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
  
 -              if ( $oldtext !== false  || $newtext != '' ) {
 +        if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
-                       $oldtitle = wfMsgExt( 'currentrev', array( 'parseinline' ) );
+                       $oldtitle = wfMsgExt( $oldtitlemsg, array( 'parseinline' ) );
                        $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) );
  
-             $de = $oldContent->getContentHandler()->getDifferenceEngine( $this->mArticle->getContext() );
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $oldtext, $newtext );
++            $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
 +            $de->setContent( $oldContent, $newContent );
 +
                        $difftext = $de->getDiff( $oldtitle, $newtitle );
                        $de->showDiffStyle();
                } else {
                if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $this->textbox2, $this->textbox1 );
 +                      $content1 = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
 +                      $content2 = ContentHandler::makeContent( $this->textbox2, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
 +
 +                      $handler = ContentHandler::getForModelName( $this->content_model );
 +                      $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $content2, $content1 );
                        $de->showDiff( wfMsgExt( 'yourtext', 'parseinline' ), wfMsg( 'storedversion' ) );
  
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
         * @return string
         */
        function getPreviewText() {
-               global $wgOut, $wgUser, $wgParser, $wgRawHtml;
+               global $wgOut, $wgUser, $wgParser, $wgRawHtml, $wgLang;
  
                wfProfileIn( __METHOD__ );
  
                        return $parsedNote;
                }
  
 -              if ( $this->mTriedSave && !$this->mTokenOk ) {
 -                      if ( $this->mTokenOkExceptSuffix ) {
 -                              $note = wfMsg( 'token_suffix_mismatch' );
 -                      } else {
 -                              $note = wfMsg( 'session_fail_preview' );
 -                      }
 -              } elseif ( $this->incompleteForm ) {
 -                      $note = wfMsg( 'edit_form_incomplete' );
 -              } else {
 -                      $note = wfMsg( 'previewnote' ) .
 -                              ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMsg( 'continue-editing' ) . ']]';
 -              }
 -
 -              $parserOptions = ParserOptions::newFromUser( $wgUser );
 -              $parserOptions->setEditSection( false );
 -              $parserOptions->setTidy( true );
 -              $parserOptions->setIsPreview( true );
 -              $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
 -
 -              # don't parse non-wikitext pages, show message about preview
 -              if ( $this->mTitle->isCssJsSubpage() || !$this->mTitle->isWikitextPage() ) {
 -                      if ( $this->mTitle->isCssJsSubpage() ) {
 -                              $level = 'user';
 -                      } elseif ( $this->mTitle->isCssOrJsPage() ) {
 -                              $level = 'site';
 -                      } else {
 -                              $level = false;
 -                      }
++              $note = '';
++
 +        try {
-             $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
++                  $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
  
-             if ( $this->mTriedSave && !$this->mTokenOk ) {
-                 if ( $this->mTokenOkExceptSuffix ) {
-                     $note = wfMsg( 'token_suffix_mismatch' );
-                 } else {
-                     $note = wfMsg( 'session_fail_preview' );
-                 }
-             } elseif ( $this->incompleteForm ) {
-                 $note = wfMsg( 'edit_form_incomplete' );
-             } elseif ( $this->isCssJsSubpage || $this->mTitle->isCssOrJsPage() ) {
-                 # if this is a CSS or JS page used in the UI, show a special notice
-                 # XXX: stupid php bug won't let us use $this->getContextTitle()->isCssJsSubpage() here -- This note has been there since r3530. Sure the bug was fixed time ago?
 -                      # Used messages to make sure grep find them:
 -                      # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 -                      $class = 'mw-code';
 -                      if ( $level ) {
 -                              if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMsg( "{$level}csspreview" ) . "\n</div>";
 -                                      $class .= " mw-css";
 -                              } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMsg( "{$level}jspreview" ) . "\n</div>";
 -                                      $class .= " mw-js";
++                      if ( $this->mTriedSave && !$this->mTokenOk ) {
++                              if ( $this->mTokenOkExceptSuffix ) {
++                                      $note = wfMsg( 'token_suffix_mismatch' );
+                               } else {
 -                                      throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' );
++                                      $note = wfMsg( 'session_fail_preview' );
+                               }
 -                              $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
 -                              $previewHTML = $parserOutput->getText();
++                      } elseif ( $this->incompleteForm ) {
++                              $note = wfMsg( 'edit_form_incomplete' );
+                       } else {
 -                              $previewHTML = '';
 -                      }
 -
 -                      $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n";
 -              } else {
 -                      $toparse = $this->textbox1;
 -
 -                      # If we're adding a comment, we need to show the
 -                      # summary as the headline
 -                      if ( $this->section == "new" && $this->summary != "" ) {
 -                              $toparse = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $toparse;
 -                      }
 -
 -                      wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
 -
 -                      $parserOptions->enableLimitReport();
 -
 -                      $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
 -                      $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
 -
 -                      $rt = Title::newFromRedirectArray( $this->textbox1 );
 -                      if ( $rt ) {
 -                              $previewHTML = $this->mArticle->viewRedirect( $rt, false );
 -                      } else {
 -                              $previewHTML = $parserOutput->getText();
 -                      }
 -
 -                      $this->mParserOutput = $parserOutput;
 -                      $wgOut->addParserOutputNoText( $parserOutput );
 -
 -                      if ( count( $parserOutput->getWarnings() ) ) {
 -                              $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 -                      }
 -              }
++                              $note = wfMsg( 'previewnote' ) .
++                                      ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMsg( 'continue-editing' ) . ']]';
++                      }
++
++            $parserOptions = ParserOptions::newFromUser( $wgUser );
++            $parserOptions->setEditSection( false );
++            $parserOptions->setTidy( true );
++            $parserOptions->setIsPreview( true );
++            $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' );
 +
++                      if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
++                # don't parse non-wikitext pages, show message about preview
 +                if( $this->mTitle->isCssJsSubpage() ) {
 +                    $level = 'user';
 +                } elseif( $this->mTitle->isCssOrJsPage() ) {
 +                    $level = 'site';
 +                } else {
 +                    $level = false;
 +                }
 +
 +                if ( $content->getModelName() == CONTENT_MODEL_CSS ) {
 +                    $format = 'css';
 +                } elseif ( $content->getModelName() == CONTENT_MODEL_JAVASCRIPT ) {
 +                    $format = 'js';
 +                } else {
 +                    $format = false;
 +                }
 +
 +                # Used messages to make sure grep find them:
 +                # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 +                if( $level && $format ) {
 +                    $note = "<div id='mw-{$level}{$format}preview'>" . wfMsg( "{$level}{$format}preview" ) . "</div>";
 +                } else {
 +                    $note = wfMsg( 'previewnote' );
 +                }
 +            } else {
 +                $note = wfMsg( 'previewnote' );
 +            }
 +
-             $parserOptions = ParserOptions::newFromUser( $wgUser );
-             $parserOptions->setEditSection( false );
-             $parserOptions->setTidy( true );
-             $parserOptions->setIsPreview( true );
-             $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' );
 +            $rt = $content->getRedirectChain();
 +
 +            if ( $rt ) {
 +                $previewHTML = $this->mArticle->viewRedirect( $rt, false );
 +            } else {
 +
 +                # If we're adding a comment, we need to show the
 +                # summary as the headline
 +                if ( $this->section == "new" && $this->summary != "" ) {
 +                    $content = $content->addSectionHeader( $this->summary );
 +                }
 +
 +                $toparse_orig = $content->serialize( $this->content_format );
 +                $toparse = $toparse_orig;
 +                wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
 +
 +                if ( $toparse !== $toparse_orig ) {
 +                    #hook changed the text, create new Content object
 +                    $content = ContentHandler::makeContent( $toparse, $this->getTitle(), $this->content_model, $this->content_format );
 +                }
 +
 +                wfRunHooks( 'EditPageGetPreviewContent', array( $this, &$content ) ); # FIXME: document new hook
 +
 +                $parserOptions->enableLimitReport();
 +
 +                #XXX: For CSS/JS pages, we should have called the ShowRawCssJs hook here. But it's now deprecated, so never mind
 +                $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
 +
 +                              // TODO: might be a saner way to get a meaningfull context here?
 +                $parserOutput = $content->getParserOutput( $this->getArticle()->getContext(), null, $parserOptions );
 +
 +                $previewHTML = $parserOutput->getText();
 +                $this->mParserOutput = $parserOutput;
 +                $wgOut->addParserOutputNoText( $parserOutput );
 +
 +                if ( count( $parserOutput->getWarnings() ) ) {
 +                    $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                }
 +            }
 +        } catch (MWContentSerializationException $ex) {
 +            $note .= "\n\n" . wfMsg('content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +            $previewHTML = '';
 +        }
  
                if ( $this->isConflict ) {
                        $conflict = '<h2 id="mw-previewconflict">' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n";
diff --combined includes/Title.php
@@@ -271,17 -271,12 +271,17 @@@ class Title 
                        if ( isset( $row->page_is_redirect ) )
                                $this->mRedirect = (bool)$row->page_is_redirect;
                        if ( isset( $row->page_latest ) )
 -                              $this->mLatestID = (int)$row->page_latest;
 +                              $this->mLatestID = (int)$row->page_latest; # FIXME: whene3ver page_latest is updated, also update page_content_model
 +            if ( isset( $row->page_content_model ) )
 +                $this->mContentModelName = $row->page_content_model;
 +            else
 +                $this->mContentModelName = null; # initialized lazily in getContentModelName()
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
 +            $this->mContentModelName = null; # initialized lazily in getContentModelName()
                }
        }
  
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = str_replace( '_', ' ', $title );
 +        $t->mContentModelName = null; # initialized lazily in getContentModelName()
                return $t;
        }
  
         * @return Title the new object, or NULL on an error
         */
        public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
+               if ( !MWNamespace::exists( $ns ) ) {
+                       return null;
+               }
                $t = new Title();
                $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki );
                if ( $t->secureAndSplit() ) {
                return $this->mNamespace;
        }
  
 +    /**
 +     * Get the page's content model name
 +     *
 +     * @return Integer: Namespace index
 +     */
 +    public function getContentModelName() {
 +        if ( empty( $this->mContentModelName ) ) {
 +            $this->mContentModelName = ContentHandler::getDefaultModelFor( $this );
 +        }
 +
 +        return $this->mContentModelName;
 +    }
 +
 +    /**
 +     * Conveniance method for checking a title's content model name
 +     *
 +     * @param $name
 +     * @return true if $this->getContentModelName() == $name
 +     */
 +    public function hasContentModel( $name ) {
 +        return $this->getContentModelName() == $name;
 +    }
 +
        /**
         * Get the namespace text
         *
         * @return Bool
         */
        public function isWikitextPage() {
 -              $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
 -              wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
 -              return $retval;
 +              return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
        }
  
        /**
 -       * Could this page contain custom CSS or JavaScript, based
 -       * on the title?
 +       * Could this page contain custom CSS or JavaScript for the global UI.
 +     * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
 +     * or CONTENT_MODEL_JAVASCRIPT.
 +     *
 +     * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage() for that!
 +     *
 +     * Note that this method should not return true for pages that contain and show "inactive" CSS or JS.
         *
         * @return Bool
         */
        public function isCssOrJsPage() {
 -              $retval = $this->mNamespace == NS_MEDIAWIKI
 -                      && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
 -              wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
 -              return $retval;
 +        $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
 +            && ( $this->hasContentModel( CONTENT_MODEL_CSS )
 +                || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
 +
 +        #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure
 +        #      hook funktions can force this method to return true even outside the mediawiki namespace.
 +
 +        wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) );
 +
 +        return $isCssOrJsPage;
        }
  
        /**
         * @return Bool
         */
        public function isCssJsSubpage() {
 -              return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
 +              return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +                && ( $this->hasContentModel( CONTENT_MODEL_CSS )
 +                    || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
        }
  
        /**
         * @return Bool
         */
        public function isCssSubpage() {
 -              return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
 +        return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +            && $this->hasContentModel( CONTENT_MODEL_CSS ) );
        }
  
        /**
         * @return Bool
         */
        public function isJsSubpage() {
 -              return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
 +        return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +            && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
        }
  
        /**
@@@ -53,18 -53,20 +53,20 @@@ class ApiDelete extends ApiBase 
                        if ( !$titleObj ) {
                                $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
                        }
+                       $pageObj = WikiPage::factory( $titleObj );
+                       $pageObj->loadPageData( 'fromdbmaster' );
+                       if ( !$pageObj->exists() ) {
+                               $this->dieUsageMsg( 'notanarticle' );
+                       }
                } elseif ( isset( $params['pageid'] ) ) {
-                       $titleObj = Title::newFromID( $params['pageid'] );
-                       if ( !$titleObj ) {
+                       $pageObj = WikiPage::newFromID( $params['pageid'] );
+                       if ( !$pageObj ) {
                                $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) );
                        }
-               }
-               if ( !$titleObj->exists() ) {
-                       $this->dieUsageMsg( 'notanarticle' );
+                       $titleObj = $pageObj->getTitle();
                }
  
                $reason = ( isset( $params['reason'] ) ? $params['reason'] : null );
-               $pageObj = WikiPage::factory( $titleObj );
                $user = $this->getUser();
  
                if ( $titleObj->getNamespace() == NS_FILE ) {
                        // Need to pass a throwaway variable because generateReason expects
                        // a reference
                        $hasHistory = false;
 -                      $reason = $page->getAutoDeleteReason( $hasHistory );
 +                      $reason = $page->getAutoDeleteReason( $hasHistory ); #FIXME: use ContentHandler::getAutoDeleteReason()
                        if ( $reason === false ) {
                                return array( array( 'cannotdelete', $title->getPrefixedText() ) );
                        }
diff --combined includes/api/ApiMain.php
@@@ -103,7 -103,6 +103,7 @@@ class ApiMain extends ApiBase 
                'dbgfm' => 'ApiFormatDbg',
                'dump' => 'ApiFormatDump',
                'dumpfm' => 'ApiFormatDump',
 +              'none' => 'ApiFormatNone',
        );
  
        /**
                $moduleParams = $module->extractRequestParams();
  
                // Die if token required, but not provided (unless there is a gettoken parameter)
+               if ( isset( $moduleParams['gettoken'] ) ) {
+                       $gettoken = $moduleParams['gettoken'];
+               } else {
+                       $gettoken = false;
+               }
                $salt = $module->getTokenSalt();
-               if ( $salt !== false && isset( $moduleParams['gettoken'] ) && !$moduleParams['gettoken'] ) {
+               if ( $salt !== false && !$gettoken ) {
                        if ( !isset( $moduleParams['token'] ) ) {
                                $this->dieUsageMsg( array( 'missingparam', 'token' ) );
                        } else {
@@@ -887,7 -887,6 +887,7 @@@ $1'
  'portal-url'           => 'Project:Community portal',
  'privacy'              => 'Privacy policy',
  'privacypage'          => 'Project:Privacy policy',
 +'content-failed-to-parse' => "Failed to parse $2 content for $1 model: $3",
  
  'badaccess'        => 'Permission error',
  'badaccess-group0' => 'You are not allowed to execute the action you have requested.',
@@@ -1000,9 -999,6 +1000,6 @@@ Please report this to an [[Special:List
  'directorycreateerror' => 'Could not create directory "$1".',
  'filenotfound'         => 'Could not find file "$1".',
  'fileexistserror'      => 'Unable to write to file "$1": File exists.',
- 'filereadonlyerror'    => 'Unable to modify the file "$1" because the file repository "$2" is in read-only mode.
- The administrator who locked it offered this explanation: "$3".',
  'unexpected'           => 'Unexpected value: "$1"="$2".',
  'formerror'            => 'Error: Could not submit form.',
  'badarticleerror'      => 'This action cannot be performed on this page.',
@@@ -1040,6 -1036,9 +1037,9 @@@ $2'
  'ns-specialprotected'  => 'Special pages cannot be edited.',
  'titleprotected'       => 'This title has been protected from creation by [[User:$1|$1]].
  The reason given is "\'\'$2\'\'".',
+ 'filereadonlyerror'    => 'Unable to modify the file "$1" because the file repository "$2" is in read-only mode.
+ The administrator who locked it offered this explanation: "$3".',
  
  # Virus scanner
  'virus-badscanner'     => "Bad configuration: Unknown virus scanner: ''$1''",
@@@ -1373,7 -1372,8 +1373,8 @@@ Custom .css and .js pages use a lowerca
  'updated'                          => '(Updated)',
  'note'                             => "'''Note:'''",
  'previewnote'                      => "'''Remember that this is only a preview.'''
- Your changes have not yet been saved! [[#editform|→ Continue editing]]",
+ Your changes have not yet been saved!",
+ 'continue-editing'                 => "Continue editing",
  'previewconflict'                  => 'This preview reflects the text in the upper text editing area as it will appear if you choose to save.',
  'session_fail_preview'             => "'''Sorry! We could not process your edit due to a loss of session data.'''
  Please try again.
@@@ -1400,7 -1400,7 +1401,7 @@@ You will have to merge your changes int
  '''Only''' the text in the upper text area will be saved when you press \"{{int:savearticle}}\".",
  'yourtext'                         => 'Your text',
  'storedversion'                    => 'Stored revision',
- 'nonunicodebrowser'                => "'''Warning: Your browser is not unicode compliant.'''
+ 'nonunicodebrowser'                => "'''Warning: Your browser is not Unicode compliant.'''
  A workaround is in place to allow you to safely edit pages: Non-ASCII characters will appear in the edit box as hexadecimal codes.",
  'editingold'                       => "'''Warning: You are editing an out-of-date revision of this page.'''
  If you save it, any changes made since this revision will be lost.",
@@@ -1627,7 -1627,7 +1628,7 @@@ Please check the logs.'
  # Suppression log
  'suppressionlog'     => 'Suppression log',
  'suppressionlogtext' => 'Below is a list of deletions and blocks involving content hidden from administrators.
- See the [[Special:BlockList|IP block list]] for the list of currently operational bans and blocks.',
+ See the [[Special:BlockList|block list]] for the list of currently operational bans and blocks.',
  
  # History merging
  'mergehistory'                     => 'Merge page histories',
@@@ -2283,7 -2283,6 +2284,6 @@@ If the problem persists, contact an [[S
  'backend-fail-contenttype'   => 'Could not determine the content type of the file to store at "$1".',
  'backend-fail-batchsize'     => 'Storage backend given a batch of $1 file {{PLURAL:$1|operation|operations}}; the limit is $2 {{PLURAL:$2|operation|operations}}.',
  
- # File journal
  'filejournal-fail-dbconnect' => 'Could not connect to the journal database for storage backend "$1".',
  'filejournal-fail-dbquery'   => 'Could not update the journal database for storage backend "$1".',
  
@@@ -2509,7 -2508,7 +2509,7 @@@ Remember to check for other links to th
  'disambiguationspage'     => 'Template:disambig',
  'disambiguations-text'    => "The following pages link to a '''disambiguation page'''.
  They should link to the appropriate topic instead.<br />
- A page is treated as disambiguation page if it uses a template which is linked from [[MediaWiki:Disambiguationspage]]",
+ A page is treated as disambiguation page if it uses a template which is linked from [[MediaWiki:Disambiguationspage]].",
  
  'doubleredirects'                   => 'Double redirects',
  'doubleredirects-summary'           => '', # do not translate or duplicate this message to other languages
@@@ -2654,25 -2653,30 +2654,30 @@@ You can narrow down the view by selecti
  'log-title-wildcard'   => 'Search titles starting with this text',
  
  # Special:AllPages
- 'allpages'          => 'All pages',
- 'allpages-summary'  => '', # do not translate or duplicate this message to other languages
- 'alphaindexline'    => '$1 to $2',
- 'nextpage'          => 'Next page ($1)',
- 'prevpage'          => 'Previous page ($1)',
- 'allpagesfrom'      => 'Display pages starting at:',
- 'allpagesto'        => 'Display pages ending at:',
- 'allarticles'       => 'All pages',
- 'allinnamespace'    => 'All pages ($1 namespace)',
- 'allnotinnamespace' => 'All pages (not in $1 namespace)',
- 'allpagesprev'      => 'Previous',
- 'allpagesnext'      => 'Next',
- 'allpagessubmit'    => 'Go',
- 'allpagesprefix'    => 'Display pages with prefix:',
- 'allpagesbadtitle'  => 'The given page title was invalid or had an inter-language or inter-wiki prefix.
+ 'allpages'                => 'All pages',
+ 'allpages-summary'        => '', # do not translate or duplicate this message to other languages
+ 'alphaindexline'          => '$1 to $2',
+ 'nextpage'                => 'Next page ($1)',
+ 'prevpage'                => 'Previous page ($1)',
+ 'allpagesfrom'            => 'Display pages starting at:',
+ 'allpagesto'              => 'Display pages ending at:',
+ 'allarticles'             => 'All pages',
+ 'allinnamespace'          => 'All pages ($1 namespace)',
+ 'allnotinnamespace'       => 'All pages (not in $1 namespace)',
+ 'allpagesprev'            => 'Previous',
+ 'allpagesnext'            => 'Next',
+ 'allpagessubmit'          => 'Go',
+ 'allpagesprefix'          => 'Display pages with prefix:',
+ 'allpagesbadtitle'        => 'The given page title was invalid or had an inter-language or inter-wiki prefix.
  It may contain one or more characters which cannot be used in titles.',
- 'allpages-bad-ns'   => '{{SITENAME}} does not have namespace "$1".',
+ 'allpages-bad-ns'         => '{{SITENAME}} does not have namespace "$1".',
  'allpages-hide-redirects' => 'Hide redirects',
  
+ # SpecialCachedPage
+ 'cachedspecial-viewing-cached-ttl' => 'You are viewing a cached version of this page, which can be up to $1 old.',
+ 'cachedspecial-viewing-cached-ts' => 'You are viewing a cached version of this page, which might not be completely actual.',
+ 'cachedspecial-refresh-now' => 'View latest.',
  # Special:Categories
  'categories'                    => 'Categories',
  'categories-summary'            => '', # do not translate or duplicate this message to other languages
@@@ -3138,7 -3142,7 +3143,7 @@@ Fill in a specific reason below (for ex
  'badipaddress'                    => 'Invalid IP address',
  'blockipsuccesssub'               => 'Block succeeded',
  'blockipsuccesstext'              => '[[Special:Contributions/$1|$1]] has been blocked.<br />
- See [[Special:BlockList|IP block list]] to review blocks.',
+ See the [[Special:BlockList|block list]] to review blocks.',
  'ipb-blockingself'                => 'You are about to block yourself!  Are you sure you want to do that?',
  'ipb-confirmhideuser'             => 'You are about to block a user with "hide user" enabled.  This will suppress the user\'s name in all lists and log entries.  Are you sure you want to do that?',
  'ipb-edit-dropdown'               => 'Edit block reasons',
  'createaccountblock'              => 'account creation blocked',
  'emailblock'                      => 'e-mail blocked',
  'blocklist-nousertalk'            => 'cannot edit own talk page',
- 'ipblocklist-empty'               => 'The blocklist is empty.',
+ 'ipblocklist-empty'               => 'The block list is empty.',
  'ipblocklist-no-results'          => 'The requested IP address or username is not blocked.',
  'blocklink'                       => 'block',
  'unblocklink'                     => 'unblock',
@@@ -3194,7 -3198,7 +3199,7 @@@ The suppress log is provided below for 
  'reblock-logentry'                => 'changed block settings for [[$1]] with an expiry time of $2 $3',
  'blocklogtext'                    => 'This is a log of user blocking and unblocking actions.
  Automatically blocked IP addresses are not listed.
- See the [[Special:BlockList|IP block list]] for the list of currently operational bans and blocks.',
+ See the [[Special:BlockList|block list]] for the list of currently operational bans and blocks.',
  'unblocklogentry'                 => 'unblocked $1',
  'block-log-flags-anononly'        => 'anonymous users only',
  'block-log-flags-nocreate'        => 'account creation disabled',
@@@ -4619,6 -4623,14 +4624,14 @@@ You should have received [{{SERVER}}{{S
  'version-software'              => 'Installed software',
  'version-software-product'      => 'Product',
  'version-software-version'      => 'Version',
+ 'version-entrypoints'           => 'Entry point URLs',
+ 'version-entrypoints-header-entrypoint' => 'Entry point',
+ 'version-entrypoints-header-url' => 'URL',
+ 'version-entrypoints-articlepath' => '[https://www.mediawiki.org/wiki/Manual:$wgArticlePath Article path]',
+ 'version-entrypoints-scriptpath' => '[https://www.mediawiki.org/wiki/Manual:$wgScriptPath Script path]',
+ 'version-entrypoints-index-php' => '[https://www.mediawiki.org/wiki/Manual:index.php index.php]',
+ 'version-entrypoints-api-php'   => '[https://www.mediawiki.org/wiki/Manual:api.php api.php]',
+ 'version-entrypoints-load-php'  => '[https://www.mediawiki.org/wiki/Manual:load.php load.php]',
  
  # Special:FilePath
  'filepath'         => 'File path',
@@@ -4833,4 -4845,15 +4846,15 @@@ Otherwise, you can use the easy form be
  'api-error-uploaddisabled'                => 'Uploading is disabled on this wiki.',
  'api-error-verification-error'            => 'This file might be corrupt, or have the wrong extension.',
  
+ # Durations
+ 'duration-seconds'   => '$1 {{PLURAL:$1|second|seconds}}',
+ 'duration-minutes'   => '$1 {{PLURAL:$1|minute|minutes}}',
+ 'duration-hours'     => '$1 {{PLURAL:$1|hour|hours}}',
+ 'duration-days'      => '$1 {{PLURAL:$1|day|days}}',
+ 'duration-weeks'     => '$1 {{PLURAL:$1|week|weeks}}',
+ 'duration-years'     => '$1 {{PLURAL:$1|year|years}}',
+ 'duration-decades'   => '$1 {{PLURAL:$1|decade|decades}}',
+ 'duration-centuries' => '$1 {{PLURAL:$1|century|centuries}}',
+ 'duration-millennia' => '$1 {{PLURAL:$1|millennium|millennia}}',
  );
@@@ -201,7 -201,7 +201,7 @@@ class RefreshLinks extends Maintenance 
         * @param $id int The page_id
         */
        public static function fixLinksFromArticle( $id ) {
-               global $wgParser;
+               global $wgParser, $wgContLang;
  
                $title = Title::newFromID( $id );
                $dbw = wfGetDB( DB_MASTER );
  
                $dbw->begin( __METHOD__ );
  
-               $options = new ParserOptions;
+               $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
                $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() );
 +
 +        $updates = $parserOutput->getSecondaryDataUpdates( $title, false );
 +        SecondaryDataUpdate::runUpdates( $updates );
 +
 +        $dbw->commit();
 +        // TODO: We don't know what happens here.
                $update = new LinksUpdate( $title, $parserOutput, false );
                $update->doUpdate();
                $dbw->commit( __METHOD__ );