merge latest master into Wikidata branch
authordaniel <daniel.kinzler@wikimedia.de>
Mon, 8 Oct 2012 11:58:54 +0000 (13:58 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Mon, 8 Oct 2012 11:58:54 +0000 (13:58 +0200)
Change-Id: Id4e0f40c03679c13d8934a6add99b5cd86d0437d

45 files changed:
1  2 
RELEASE-NOTES-1.21
includes/Article.php
includes/AutoLoader.php
includes/DefaultSettings.php
includes/EditPage.php
includes/Export.php
includes/GlobalFunctions.php
includes/Message.php
includes/OutputPage.php
includes/SqlDataUpdate.php
includes/Title.php
includes/WikiPage.php
includes/actions/RollbackAction.php
includes/api/ApiDelete.php
includes/api/ApiEditPage.php
includes/api/ApiMain.php
includes/api/ApiQueryRevisions.php
includes/content/WikitextContent.php
includes/diff/DifferenceEngine.php
includes/filerepo/file/LocalFile.php
includes/installer/MysqlUpdater.php
includes/installer/SqliteUpdater.php
includes/job/DoubleRedirectJob.php
includes/parser/Parser.php
includes/parser/ParserOutput.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/search/SearchEngine.php
includes/specials/SpecialUndelete.php
includes/upload/UploadFromUrl.php
languages/Language.php
languages/LanguageConverter.php
languages/messages/MessagesDe.php
languages/messages/MessagesEn.php
languages/messages/MessagesGa.php
languages/messages/MessagesQqq.php
maintenance/Maintenance.php
maintenance/storage/testCompression.php
maintenance/tables.sql
tests/parser/parserTest.inc
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/LinksUpdateTest.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/filerepo/FileBackendTest.php
tests/phpunit/includes/search/SearchEngineTest.php
thumb.php

@@@ -11,20 -11,28 +11,32 @@@ MediaWiki 1.21 is an alpha-quality bran
  production.
  
  === Configuration changes in 1.21 ===
+ * (bug 29374) $wgVectorUseSimpleSearch is now enabled by default.
+ * Deprecated $wgAllowRealName is removed. Use $wgHiddenPrefs[] = 'realname' instead
  
  === New features in 1.21 ===
 +* Added ContentHandler facility to allow extensions to support other content than wikitext.
 +  See docs/contenthandler.txt for details.
  
  === Bug fixes in 1.21 ===
+ * (bug 40353) SpecialDoubleRedirect should support interwiki redirects.
+ * (bug 40352) fixDoubleRedirects.php should support interwiki redirects.
+ * (bug 9237) SpecialBrokenRedirect should not list interwiki redirects.
+ * (bug 34960) Drop unused fields rc_moved_to_ns and rc_moved_to_title from recentchanges table.
+ * (bug 32951) Do not register internal externals with absolute protocol,
+   when server has relative protocol.
  
  === API changes in 1.21 ===
 +* prop=revisions can now report the contentmodel and contentformat, see docs/contenthandler.txt
 +* action=edit and action=parse now support contentmodel and contentformat parameters to control the interpretation of
 +  page content; See docs/contenthandler.txt for details.
+ * (bug 35693) ApiQueryImageInfo now suppresses errors when unserializing metadata.
  
  === Languages updated in 1.21 ===
  
 -
+ MediaWiki supports over 350 languages. Many localisations are updated
+ regularly. Below only new and removed languages are listed, as well as
+ changes to languages because of Bugzilla reports.
  === Other changes in 1.21 ===
  
  == Compatibility ==
@@@ -455,10 -380,10 +455,10 @@@ class Article extends Page 
  
                // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks.
                // We should instead work with the Revision object when we need it...
-               $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER ); // Loads if user is allowed
 -              $this->mContent = $this->mRevision->getText( Revision::FOR_THIS_USER, $this->getContext()->getUser() ); // Loads if user is allowed
++              $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER, $this->getContext()->getUser() ); // Loads if user is allowed
                $this->mRevIdFetched = $this->mRevision->getId();
  
 -              wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
 +              wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) );
  
                wfProfileOut( __METHOD__ );
  
Simple merge
Simple merge
@@@ -1047,16 -946,16 +1053,16 @@@ class EditPage 
  
                $title = Title::newFromText( $preload );
                # Check for existence to avoid getting MediaWiki:Noarticletext
-               if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
+               if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
 -                      return '';
 +                      return $handler->makeEmptyContent();
                }
  
                $page = WikiPage::factory( $title );
                if ( $page->isRedirect() ) {
                        $title = $page->getRedirectTarget();
                        # Same as before
-                       if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
+                       if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
 -                              return '';
 +                              return $handler->makeEmptyContent();
                        }
                        $page = WikiPage::factory( $title );
                }
                                        // passed.
                                        if ( $this->summary === '' ) {
                                                $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
-                                               $this->summary = wfMessage( 'newsectionsummary', $cleanSectionTitle )
-                                                       ->inContentLanguage()->text() ;
+                                               $this->summary = wfMessage( 'newsectionsummary' )
 -                                                      ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
++                                                      ->rawParams( $cleanSectionTitle )->inContentLanguage()->text() ;
                                        }
                                } elseif ( $this->summary !== '' ) {
                                        // Insert the section title above the content.
                        ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
                        ( $bot ? EDIT_FORCE_BOT : 0 );
  
 -              $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags );
 +                      $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags,
 +                                                                                                                      false, null, $this->content_format );
  
                if ( $doEditStatus->isOK() ) {
-                               $result['redirect'] = $content->isRedirect();
-                       $this->commitWatch();
 -                      $result['redirect'] = Title::newFromRedirect( $text ) !== null;
++                      $result['redirect'] = $content->isRedirect();
+                       $this->updateWatchlist();
                        wfProfileOut( __METHOD__ );
                        return $status;
                } else {
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
@@@ -597,41 -554,21 +597,44 @@@ class WikiPage extends Page implements 
                return null;
        }
  
-       public function getContent( $audience = Revision::FOR_PUBLIC ) {
 +      /**
 +       * Get the content of the current revision. No side-effects...
 +       *
 +       * @param $audience Integer: one of:
 +       *      Revision::FOR_PUBLIC       to be displayed to all users
 +       *      Revision::FOR_THIS_USER    to be displayed to $wgUser
 +       *      Revision::RAW              get the text regardless of permissions
++       * @param $user User object to check for, only if FOR_THIS_USER is passed
++       *              to the $audience parameter
 +       * @return Content|null The content of the current revision
 +       *
 +       * @since 1.21
 +       */
++      public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
 +              $this->loadLastEdit();
 +              if ( $this->mLastRevision ) {
 +                      return $this->mLastRevision->getContent( $audience );
 +              }
 +              return null;
 +      }
 +
        /**
         * Get the text of the current revision. No side-effects...
         *
         * @param $audience Integer: one of:
         *      Revision::FOR_PUBLIC       to be displayed to all users
-        *      Revision::FOR_THIS_USER    to be displayed to $wgUser
+        *      Revision::FOR_THIS_USER    to be displayed to the given user
         *      Revision::RAW              get the text regardless of permissions
 -       * @return String|bool The text of the current revision. False on failure
+        * @param $user User object to check for, only if FOR_THIS_USER is passed
+        *              to the $audience parameter
 +       * @return String|false The text of the current revision
 +       * @deprecated as of 1.21, getContent() should be used instead.
         */
-       public function getText( $audience = Revision::FOR_PUBLIC ) { #@todo: deprecated, replace usage!
 -      public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
++      public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) { #@todo: deprecated, replace usage!
 +              wfDeprecated( __METHOD__, '1.21' );
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
-                       return $this->mLastRevision->getText( $audience );
+                       return $this->mLastRevision->getText( $audience, $user );
                }
                return false;
        }
Simple merge
Simple merge
@@@ -82,9 -60,11 +82,9 @@@ class ApiEditPage extends ApiBase 
                        if ( $titleObj->isRedirect() ) {
                                $oldTitle = $titleObj;
  
 -                              $titles = Title::newFromRedirectArray(
 -                                      Revision::newFromTitle(
 -                                              $oldTitle, false, Revision::READ_LATEST
 -                                      )->getText( Revision::FOR_THIS_USER, $user )
 -                              );
 +                              $titles = Revision::newFromTitle( $oldTitle, false, Revision::READ_LATEST )
-                                                       ->getContent( Revision::FOR_THIS_USER )
++                                                      ->getContent( Revision::FOR_THIS_USER, $user )
 +                                                      ->getRedirectChain();
                                // array_shift( $titles );
  
                                $redirValues = array();
Simple merge
Simple merge
index 465a402,0000000..9526520
mode 100644,000000..100644
--- /dev/null
@@@ -1,289 -1,0 +1,289 @@@
-                               ->inContentLanguage()->params( $header )->text();
 +<?php
 +/**
 + * @since 1.21
 + */
 +class WikitextContent extends TextContent {
 +
 +      public function __construct( $text ) {
 +              parent::__construct( $text, CONTENT_MODEL_WIKITEXT );
 +      }
 +
 +      /**
 +       * @see Content::getSection()
 +       */
 +      public function getSection( $section ) {
 +              global $wgParser;
 +
 +              $text = $this->getNativeData();
 +              $sect = $wgParser->getSection( $text, $section, false );
 +
 +              return  new WikitextContent( $sect );
 +      }
 +
 +      /**
 +       * @see Content::replaceSection()
 +       */
 +      public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
 +              wfProfileIn( __METHOD__ );
 +
 +              $myModelId = $this->getModel();
 +              $sectionModelId = $with->getModel();
 +
 +              if ( $sectionModelId != $myModelId  ) {
 +                      throw new MWException( "Incompatible content model for section: " .
 +                              "document uses $myModelId but " .
 +                              "section uses $sectionModelId." );
 +              }
 +
 +              $oldtext = $this->getNativeData();
 +              $text = $with->getNativeData();
 +
 +              if ( $section === '' ) {
 +                      return $with; # XXX: copy first?
 +              } if ( $section == 'new' ) {
 +                      # Inserting a new section
 +                      $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' )
 +                              ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : '';
 +                      if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
 +                              $text = strlen( trim( $oldtext ) ) > 0
 +                                      ? "{$oldtext}\n\n{$subject}{$text}"
 +                                      : "{$subject}{$text}";
 +                      }
 +              } else {
 +                      # Replacing an existing section; roll out the big guns
 +                      global $wgParser;
 +
 +                      $text = $wgParser->replaceSection( $oldtext, $section, $text );
 +              }
 +
 +              $newContent = new WikitextContent( $text );
 +
 +              wfProfileOut( __METHOD__ );
 +              return $newContent;
 +      }
 +
 +      /**
 +       * Returns a new WikitextContent object with the given section heading
 +       * prepended.
 +       *
 +       * @param $header string
 +       * @return Content
 +       */
 +      public function addSectionHeader( $header ) {
 +              $text = wfMessage( 'newsectionheaderdefaultlevel' )
++                      ->rawParams( $header )->inContentLanguage()->text();
 +              $text .= "\n\n";
 +              $text .= $this->getNativeData();
 +
 +              return new WikitextContent( $text );
 +      }
 +
 +      /**
 +       * Returns a Content object with pre-save transformations applied using
 +       * Parser::preSaveTransform().
 +       *
 +       * @param $title Title
 +       * @param $user User
 +       * @param $popts ParserOptions
 +       * @return Content
 +       */
 +      public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
 +              global $wgParser;
 +
 +              $text = $this->getNativeData();
 +              $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
 +
 +              return new WikitextContent( $pst );
 +      }
 +
 +      /**
 +       * Returns a Content object with preload transformations applied (or this
 +       * object if no transformations apply).
 +       *
 +       * @param $title Title
 +       * @param $popts ParserOptions
 +       * @return Content
 +       */
 +      public function preloadTransform( Title $title, ParserOptions $popts ) {
 +              global $wgParser;
 +
 +              $text = $this->getNativeData();
 +              $plt = $wgParser->getPreloadText( $text, $title, $popts );
 +
 +              return new WikitextContent( $plt );
 +      }
 +
 +      /**
 +       * Implement redirect extraction for wikitext.
 +       *
 +       * @return null|Title
 +       *
 +       * @note: migrated here from Title::newFromRedirectInternal()
 +       *
 +       * @see Content::getRedirectTarget
 +       * @see AbstractContent::getRedirectTarget
 +       */
 +      public function getRedirectTarget() {
 +              global $wgMaxRedirects;
 +              if ( $wgMaxRedirects < 1 ) {
 +                      // redirects are disabled, so quit early
 +                      return null;
 +              }
 +              $redir = MagicWord::get( 'redirect' );
 +              $text = trim( $this->getNativeData() );
 +              if ( $redir->matchStartAndRemove( $text ) ) {
 +                      // Extract the first link and see if it's usable
 +                      // Ensure that it really does come directly after #REDIRECT
 +                      // Some older redirects included a colon, so don't freak about that!
 +                      $m = array();
 +                      if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
 +                              // Strip preceding colon used to "escape" categories, etc.
 +                              // and URL-decode links
 +                              if ( strpos( $m[1], '%' ) !== false ) {
 +                                      // Match behavior of inline link parsing here;
 +                                      $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
 +                              }
 +                              $title = Title::newFromText( $m[1] );
 +                              // If the title is a redirect to bad special pages or is invalid, return null
 +                              if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
 +                                      return null;
 +                              }
 +                              return $title;
 +                      }
 +              }
 +              return null;
 +      }
 +
 +      /**
 +       * @see   Content::updateRedirect()
 +       *
 +       * This implementation replaces the first link on the page with the given new target
 +       * if this Content object is a redirect. Otherwise, this method returns $this.
 +       *
 +       * @since 1.21
 +       *
 +       * @param Title $target
 +       *
 +       * @return Content a new Content object with the updated redirect (or $this if this Content object isn't a redirect)
 +       */
 +      public function updateRedirect( Title $target ) {
 +              if ( !$this->isRedirect() ) {
 +                      return $this;
 +              }
 +
 +              # Fix the text
 +              # Remember that redirect pages can have categories, templates, etc.,
 +              # so the regex has to be fairly general
 +              $newText = preg_replace( '/ \[ \[  [^\]]*  \] \] /x',
 +                      '[[' . $target->getFullText() . ']]',
 +                      $this->getNativeData(), 1 );
 +
 +              return new WikitextContent( $newText );
 +      }
 +
 +      /**
 +       * Returns true if this content is not a redirect, and this content's text
 +       * is countable according to the criteria defined by $wgArticleCountMethod.
 +       *
 +       * @param $hasLinks Bool  if it is known whether this content contains
 +       *    links, provide this information here, to avoid redundant parsing to
 +       *    find out.
 +       * @param $title null|\Title
 +       *
 +       * @internal param \IContextSource $context context for parsing if necessary
 +       *
 +       * @return bool True if the content is countable
 +       */
 +      public function isCountable( $hasLinks = null, Title $title = null ) {
 +              global $wgArticleCountMethod;
 +
 +              if ( $this->isRedirect( ) ) {
 +                      return false;
 +              }
 +
 +              $text = $this->getNativeData();
 +
 +              switch ( $wgArticleCountMethod ) {
 +                      case 'any':
 +                              return true;
 +                      case 'comma':
 +                              return strpos( $text,  ',' ) !== false;
 +                      case 'link':
 +                              if ( $hasLinks === null ) { # not known, find out
 +                                      if ( !$title ) {
 +                                              $context = RequestContext::getMain();
 +                                              $title = $context->getTitle();
 +                                      }
 +
 +                                      $po = $this->getParserOutput( $title, null, null, false );
 +                                      $links = $po->getLinks();
 +                                      $hasLinks = !empty( $links );
 +                              }
 +
 +                              return $hasLinks;
 +              }
 +
 +              return false;
 +      }
 +
 +      public function getTextForSummary( $maxlength = 250 ) {
 +              $truncatedtext = parent::getTextForSummary( $maxlength );
 +
 +              # clean up unfinished links
 +              # XXX: make this optional? wasn't there in autosummary, but required for
 +              # deletion summary.
 +              $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );
 +
 +              return $truncatedtext;
 +      }
 +
 +      /**
 +       * Returns a ParserOutput object resulting from parsing the content's text
 +       * using $wgParser.
 +       *
 +       * @since    1.21
 +       *
 +       * @param $content Content the content to render
 +       * @param $title \Title
 +       * @param $revId null
 +       * @param $options null|ParserOptions
 +       * @param $generateHtml bool
 +       *
 +       * @internal param \IContextSource|null $context
 +       * @return ParserOutput representing the HTML form of the text
 +       */
 +      public function getParserOutput( Title $title,
 +              $revId = null,
 +              ParserOptions $options = null, $generateHtml = true
 +      ) {
 +              global $wgParser;
 +
 +              if ( !$options ) {
 +                      //NOTE: use canonical options per default to produce cacheable output
 +                      $options = $this->getContentHandler()->makeParserOptions( 'canonical' );
 +              }
 +
 +              $po = $wgParser->parse( $this->getNativeData(), $title, $options, true, true, $revId );
 +              return $po;
 +      }
 +
 +      protected function getHtml() {
 +              throw new MWException(
 +                      "getHtml() not implemented for wikitext. "
 +                              . "Use getParserOutput()->getText()."
 +              );
 +      }
 +
 +      /**
 +       * @see  Content::matchMagicWord()
 +       *
 +       * This implementation calls $word->match() on the this TextContent object's text.
 +       *
 +       * @param MagicWord $word
 +       *
 +       * @return bool whether this Content object matches the given magic word.
 +       */
 +      public function matchMagicWord( MagicWord $word ) {
 +              return $word->match( $this->getNativeData() );
 +      }
 +}
@@@ -1150,14 -1082,14 +1150,14 @@@ class DifferenceEngine extends ContextS
                        return false;
                }
                if ( $this->mOldRev ) {
-                       $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER );
 -                      $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER, $this->getUser() );
 -                      if ( $this->mOldtext === false ) {
++                      $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
 +                      if ( $this->mOldContent === false ) {
                                return false;
                        }
                }
                if ( $this->mNewRev ) {
-                       $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
 -                      $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER, $this->getUser() );
 -                      if ( $this->mNewtext === false ) {
++                      $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
 +                      if ( $this->mNewContent === false ) {
                                return false;
                        }
                }
                if ( !$this->loadRevisionData() ) {
                        return false;
                }
-               $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
 -              $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER, $this->getUser() );
++              $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
                return true;
        }
  }
Simple merge
@@@ -217,11 -216,8 +217,13 @@@ class MysqlUpdater extends DatabaseUpda
                        array( 'dropField', 'category',     'cat_hidden',       'patch-cat_hidden.sql' ),
  
                        // 1.21
 +                      array( 'addField',      'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
 +                      array( 'addField',      'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
 +                      array( 'addField',      'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
+                       array( 'dropField', 'site_stats',   'ss_admins',        'patch-drop-ss_admins.sql' ),
+                       array( 'dropField', 'recentchanges', 'rc_moved_to_title',            'patch-rc_moved.sql' ),
                );
        }
  
@@@ -96,11 -95,8 +96,13 @@@ class SqliteUpdater extends DatabaseUpd
                        array( 'dropField', 'category',     'cat_hidden',       'patch-cat_hidden.sql' ),
  
                        // 1.21
 +                      array( 'addField',      'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
 +                      array( 'addField',      'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
 +                      array( 'addField',      'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
+                       array( 'dropField', 'site_stats',   'ss_admins',        'patch-drop-ss_admins.sql' ),
+                       array( 'dropField', 'recentchanges', 'rc_moved_to_title', 'patch-rc_moved.sql' ),
                );
        }
  
@@@ -122,13 -121,17 +121,13 @@@ class DoubleRedirectJob extends Job 
  
                # Preserve fragment (bug 14904)
                $newTitle = Title::makeTitle( $newTitle->getNamespace(), $newTitle->getDBkey(),
-                       $currentDest->getFragment() );
+                       $currentDest->getFragment(), $newTitle->getInterwiki() );
  
                # Fix the text
 -              # Remember that redirect pages can have categories, templates, etc.,
 -              # so the regex has to be fairly general
 -              $newText = preg_replace( '/ \[ \[  [^\]]*  \] \] /x',
 -                      '[[' . $newTitle->getFullText() . ']]',
 -                      $text, 1 );
 -
 -              if ( $newText === $text ) {
 -                      $this->setLastError( 'Text unchanged???' );
 +              $newContent = $content->updateRedirect( $newTitle );
 +
 +              if ( $newContent->equals( $content ) ) {
 +                      $this->setLastError( 'Content unchanged???' );
                        return false;
                }
  
@@@ -1948,8 -1961,11 +1961,14 @@@ class Parser 
                                # Interwikis
                                wfProfileIn( __METHOD__."-interwiki" );
                                if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' ) ) {
-                                       // FIXME: the above check prevents links to sites with identifiers that are not language codes
-                                       $this->mOutput->addLanguageLink( $nt->getFullText() );
++                                      // XXX: the above check prevents links to sites with identifiers that are not language codes
++
+                                       # Bug 24502: filter duplicates
+                                       if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
+                                               $this->mLangLinkLanguages[$iw] = true;
+                                               $this->mOutput->addLanguageLink( $nt->getFullText() );
+                                       }
++
                                        $s = rtrim( $s . $prefix );
                                        $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
                                        wfProfileOut( __METHOD__."-interwiki" );
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
@@@ -143,12 -148,10 +148,12 @@@ class LinksUpdateTest extends MediaWiki
  
        #@todo: test recursive, too!
  
-       protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, Array $expectedRows ) {
+       protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, array $expectedRows ) {
                $update = new LinksUpdate( $title, $parserOutput );
  
 +              $update->beginTransaction();
                $update->doUpdate();
 +              $update->commitTransaction();
  
                $this->assertSelect( $table, $fields, $condition, $expectedRows );
        }
Simple merge
diff --cc thumb.php
Simple merge