Merge "ApiQueryWatchlist: Handle empty wltypes"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 21 Dec 2016 05:34:04 +0000 (05:34 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 21 Dec 2016 05:34:04 +0000 (05:34 +0000)
14 files changed:
RELEASE-NOTES-1.29
autoload.php
docs/hooks.txt
includes/api/ApiMain.php
includes/api/ApiMove.php
includes/api/ApiTag.php
includes/api/ApiValidatePassword.php [new file with mode: 0644]
includes/api/i18n/en.json
includes/api/i18n/qqq.json
includes/libs/StringUtils.php
includes/parser/Parser.php
includes/specials/SpecialUpload.php
includes/upload/UploadBase.php
tests/parser/parserTests.txt

index 3af1654..10b152d 100644 (file)
@@ -75,6 +75,8 @@ production.
   'stasherrors' rather than a 'stashfailed' text string.
 * action=watch reports 'errors' and 'warnings' instead of a single 'error', and
   no longer returns a 'message' on success.
+* Added action=validatepassword to validate passwords for the account creation
+  and password change forms.
 
 === Action API internal changes in 1.29 ===
 * New methods were added to ApiBase to handle errors and warnings using i18n
index 941b335..d85b679 100644 (file)
@@ -147,6 +147,7 @@ $wgAutoloadLocalClasses = [
        'ApiUpload' => __DIR__ . '/includes/api/ApiUpload.php',
        'ApiUsageException' => __DIR__ . '/includes/api/ApiUsageException.php',
        'ApiUserrights' => __DIR__ . '/includes/api/ApiUserrights.php',
+       'ApiValidatePassword' => __DIR__ . '/includes/api/ApiValidatePassword.php',
        'ApiWatch' => __DIR__ . '/includes/api/ApiWatch.php',
        'ArchivedFile' => __DIR__ . '/includes/filerepo/file/ArchivedFile.php',
        'ArrayDiffFormatter' => __DIR__ . '/includes/diff/ArrayDiffFormatter.php',
index 1b543b5..b165285 100644 (file)
@@ -592,6 +592,10 @@ Use this hook to extend action=tokens with new token types.
 &$tokenTypes: supported token types in format 'type' => callback function
   used to retrieve this type of tokens.
 
+'ApiValidatePassword': Called from ApiValidatePassword.
+$module: ApiValidatePassword instance.
+&$r: Result array.
+
 'Article::MissingArticleConditions': Before fetching deletion & move log entries
 to display a message of a non-existing page being deleted/moved, give extensions
 a chance to hide their (unrelated) log entries.
index 54679a8..4220fb8 100644 (file)
@@ -79,6 +79,7 @@ class ApiMain extends ApiBase {
                'tokens' => 'ApiTokens',
                'checktoken' => 'ApiCheckToken',
                'cspreport' => 'ApiCSPReport',
+               'validatepassword' => 'ApiValidatePassword',
 
                // Write modules
                'purge' => 'ApiPurge',
index 7c8aa90..18e582d 100644 (file)
@@ -59,7 +59,7 @@ class ApiMove extends ApiBase {
                if ( !$toTitle || $toTitle->isExternal() ) {
                        $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['to'] ) ] );
                }
-               $toTalk = $toTitle->getTalkPage();
+               $toTalk = $toTitle->canTalk() ? $toTitle->getTalkPage() : null;
 
                if ( $toTitle->getNamespace() == NS_FILE
                        && !RepoGroup::singleton()->getLocalRepo()->findFile( $toTitle )
@@ -100,7 +100,7 @@ class ApiMove extends ApiBase {
                $r['moveoverredirect'] = $toTitleExists;
 
                // Move the talk page
-               if ( $params['movetalk'] && $fromTalk->exists() && !$fromTitle->isTalkPage() ) {
+               if ( $params['movetalk'] && $toTalk && $fromTalk->exists() && !$fromTitle->isTalkPage() ) {
                        $toTalkExists = $toTalk->exists();
                        $status = $this->movePage( $fromTalk, $toTalk, $params['reason'], !$params['noredirect'] );
                        if ( $status->isOK() ) {
index f6c0584..b142900 100644 (file)
@@ -109,7 +109,7 @@ class ApiTag extends ApiBase {
                } else {
                        $idResult['status'] = 'success';
                        if ( is_null( $status->value->logId ) ) {
-                               $idResult['noop'] = '';
+                               $idResult['noop'] = true;
                        } else {
                                $idResult['actionlogid'] = $status->value->logId;
                                $idResult['added'] = $status->value->addedTags;
diff --git a/includes/api/ApiValidatePassword.php b/includes/api/ApiValidatePassword.php
new file mode 100644 (file)
index 0000000..6968523
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+
+use MediaWiki\Auth\AuthManager;
+
+/**
+ * @ingroup API
+ */
+class ApiValidatePassword extends ApiBase {
+
+       public function execute() {
+               $params = $this->extractRequestParams();
+
+               // For sanity
+               $this->requirePostedParameters( [ 'password' ] );
+
+               if ( $params['user'] !== null ) {
+                       $user = User::newFromName( $params['user'], 'creatable' );
+                       if ( !$user ) {
+                               $encParamName = $this->encodeParamName( 'user' );
+                               $this->dieWithError(
+                                       [ 'apierror-baduser', $encParamName, wfEscapeWikiText( $params['user'] ) ],
+                                       "baduser_{$encParamName}"
+                               );
+                       }
+
+                       if ( !$user->isAnon() || AuthManager::singleton()->userExists( $user->getName() ) ) {
+                               $this->dieWithError( 'userexists' );
+                       }
+
+                       $user->setEmail( (string)$params['email'] );
+                       $user->setRealName( (string)$params['realname'] );
+               } else {
+                       $user = $this->getUser();
+               }
+
+               $validity = $user->checkPasswordValidity( $params['password'] );
+               $r['validity'] = $validity->isGood() ? 'Good' : ( $validity->isOK() ? 'Change' : 'Invalid' );
+               $messages = array_merge(
+                       $this->getErrorFormatter()->arrayFromStatus( $validity, 'error' ),
+                       $this->getErrorFormatter()->arrayFromStatus( $validity, 'warning' )
+               );
+               if ( $messages ) {
+                       $r['validitymessages'] = $messages;
+               }
+
+               Hooks::run( 'ApiValidatePassword', [ $this, &$r ] );
+
+               $this->getResult()->addValue( null, $this->getModuleName(), $r );
+       }
+
+       public function mustBePosted() {
+               return true;
+       }
+
+       public function getAllowedParams() {
+               return [
+                       'password' => [
+                               ApiBase::PARAM_TYPE => 'password',
+                               ApiBase::PARAM_REQUIRED => true
+                       ],
+                       'user' => [
+                               ApiBase::PARAM_TYPE => 'user',
+                       ],
+                       'email' => null,
+                       'realname' => null,
+               ];
+       }
+
+       protected function getExamplesMessages() {
+               return [
+                       'action=validatepassword&password=foobar'
+                               => 'apihelp-validatepassword-example-1',
+                       'action=validatepassword&password=querty&user=Example'
+                               => 'apihelp-validatepassword-example-2',
+               ];
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Validatepassword';
+       }
+}
index d748894..f6eeffe 100644 (file)
        "apihelp-userrights-example-user": "Add user <kbd>FooBot</kbd> to group <kbd>bot</kbd>, and remove from groups <kbd>sysop</kbd> and <kbd>bureaucrat</kbd>.",
        "apihelp-userrights-example-userid": "Add the user with ID <kbd>123</kbd> to group <kbd>bot</kbd>, and remove from groups <kbd>sysop</kbd> and <kbd>bureaucrat</kbd>.",
 
+       "apihelp-validatepassword-description": "Validate a password against the wiki's password policies.\n\nValidity is reported as <samp>Good</samp> if the password is acceptable, <samp>Change</samp> if the password may be used for login but must be changed, or <samp>Invalid</samp> if the password is not usable.",
+       "apihelp-validatepassword-param-password": "Password to validate.",
+       "apihelp-validatepassword-param-user": "User name, for use when testing account creation. The named user must not exist.",
+       "apihelp-validatepassword-param-email": "Email address, for use when testing account creation.",
+       "apihelp-validatepassword-param-realname": "Real name, for use when testing account creation.",
+       "apihelp-validatepassword-example-1": "Validate the password <kbd>foobar</kbd> for the current user.",
+       "apihelp-validatepassword-example-2": "Validate the password <kbd>qwerty</kbd> for creating user <kbd>Example</kbd>.",
+
        "apihelp-watch-description": "Add or remove pages from the current user's watchlist.",
        "apihelp-watch-param-title": "The page to (un)watch. Use <var>$1titles</var> instead.",
        "apihelp-watch-param-unwatch": "If set the page will be unwatched rather than watched.",
index 2bdc64a..1fbc3d0 100644 (file)
        "apihelp-userrights-param-reason": "{{doc-apihelp-param|userrights|reason}}",
        "apihelp-userrights-example-user": "{{doc-apihelp-example|userrights}}",
        "apihelp-userrights-example-userid": "{{doc-apihelp-example|userrights}}",
+       "apihelp-validatepassword-description": "{{doc-apihelp-description|validatepassword}}",
+       "apihelp-validatepassword-param-email": "{{doc-apihelp-param|validatepassword|email}}",
+       "apihelp-validatepassword-param-password": "{{doc-apihelp-param|validatepassword|password}}",
+       "apihelp-validatepassword-param-realname": "{{doc-apihelp-param|validatepassword|realname}}",
+       "apihelp-validatepassword-param-user": "{{doc-apihelp-param|validatepassword|user}}",
+       "apihelp-validatepassword-example-1": "{{doc-apihelp-example|validatepassword}}",
+       "apihelp-validatepassword-example-2": "{{doc-apihelp-example|validatepassword}}",
        "apihelp-watch-description": "{{doc-apihelp-description|watch}}",
        "apihelp-watch-param-title": "{{doc-apihelp-param|watch|title}}",
        "apihelp-watch-param-unwatch": "{{doc-apihelp-param|watch|unwatch}}",
index 6b10c09..26f3c4a 100644 (file)
@@ -54,6 +54,59 @@ class StringUtils {
                        ( $newPHP || preg_match( "/\xf4[\x90-\xbf]|[\xf5-\xff]/S", $value ) === 0 );
        }
 
+       /**
+        * Explode a string, but ignore any instances of the separator inside
+        * the given start and end delimiters, which may optionally nest.
+        * The delimiters are literal strings, not regular expressions.
+        * @param string $startDelim Start delimiter
+        * @param string $endDelim End delimiter
+        * @param string $separator Separator string for the explode.
+        * @param string $subject Subject string to explode.
+        * @param bool $nested True iff the delimiters are allowed to nest.
+        * @return ArrayIterator
+        */
+       static function delimiterExplode( $startDelim, $endDelim, $separator,
+               $subject, $nested = false ) {
+               $inputPos = 0;
+               $lastPos = 0;
+               $depth = 0;
+               $encStart = preg_quote( $startDelim, '!' );
+               $encEnd = preg_quote( $endDelim, '!' );
+               $encSep = preg_quote( $separator, '!' );
+               $len = strlen( $subject );
+               $m = [];
+               $exploded = [];
+               while (
+                       $inputPos < $len &&
+                       preg_match(
+                               "!$encStart|$encEnd|$encSep!S", $subject, $m,
+                               PREG_OFFSET_CAPTURE, $inputPos
+                       )
+               ) {
+                       $match = $m[0][0];
+                       $matchPos = $m[0][1];
+                       $inputPos = $matchPos + strlen( $match );
+                       if ( $match === $separator ) {
+                               if ( $depth === 0 ) {
+                                       $exploded[] = substr(
+                                               $subject, $lastPos, $matchPos - $lastPos
+                                       );
+                                       $lastPos = $inputPos;
+                               }
+                       } elseif ( $match === $startDelim ) {
+                               if ( $depth === 0 || $nested ) {
+                                       $depth++;
+                               }
+                       } else {
+                               $depth--;
+                       }
+               }
+               $exploded[] = substr( $subject, $lastPos );
+               // This method could be rewritten in the future to avoid creating an
+               // intermediate array, since the return type is just an iterator.
+               return new ArrayIterator( $exploded );
+       }
+
        /**
         * Perform an operation equivalent to `preg_replace()`
         *
index 7418547..5b2dadd 100644 (file)
@@ -5018,7 +5018,10 @@ class Parser {
                                // FIXME: Doing recursiveTagParse at this stage, and the trim before
                                // splitting on '|' is a bit odd, and different from makeImage.
                                $matches[3] = $this->recursiveTagParse( trim( $matches[3] ) );
-                               $parameterMatches = StringUtils::explode( '|', $matches[3] );
+                               // Protect LanguageConverter markup
+                               $parameterMatches = StringUtils::delimiterExplode(
+                                       '-{', '}-', '|', $matches[3], true /* nested */
+                               );
 
                                foreach ( $parameterMatches as $parameterMatch ) {
                                        list( $magicName, $match ) = $mwArray->matchVariableStartToEnd( $parameterMatch );
@@ -5035,6 +5038,11 @@ class Parser {
                                                        $addr = self::EXT_LINK_ADDR;
                                                        $prots = $this->mUrlProtocols;
                                                        // check to see if link matches an absolute url, if not then it must be a wiki link.
+                                                       if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
+                                                               // Result of LanguageConverter::markNoConversion
+                                                               // invoked on an external link.
+                                                               $linkValue = substr( $linkValue, 4, -2 );
+                                                       }
                                                        if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
                                                                $link = $linkValue;
                                                        } else {
@@ -5150,7 +5158,10 @@ class Parser {
                #  * bottom
                #  * text-bottom
 
-               $parts = StringUtils::explode( "|", $options );
+               # Protect LanguageConverter markup when splitting into parts
+               $parts = StringUtils::delimiterExplode(
+                       '-{', '}-', '|', $options, true /* allow nesting */
+               );
 
                # Give extensions a chance to select the file revision for us
                $options = [];
index f7e46cb..aabd450 100644 (file)
@@ -209,7 +209,9 @@ class SpecialUpload extends SpecialPage {
                        $this->processUpload();
                } else {
                        # Backwards compatibility hook
-                       if ( !Hooks::run( 'UploadForm:initial', [ &$this ] ) ) {
+                       // Avoid PHP 7.1 warning of passing $this by reference
+                       $upload = $this;
+                       if ( !Hooks::run( 'UploadForm:initial', [ &$upload ] ) ) {
                                wfDebug( "Hook 'UploadForm:initial' broke output of the upload form\n" );
 
                                return;
@@ -483,8 +485,9 @@ class SpecialUpload extends SpecialPage {
 
                        return;
                }
-
-               if ( !Hooks::run( 'UploadForm:BeforeProcessing', [ &$this ] ) ) {
+               // Avoid PHP 7.1 warning of passing $this by reference
+               $upload = $this;
+               if ( !Hooks::run( 'UploadForm:BeforeProcessing', [ &$upload ] ) ) {
                        wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file.\n" );
                        // This code path is deprecated. If you want to break upload processing
                        // do so by hooking into the appropriate hooks in UploadBase::verifyUpload
@@ -570,7 +573,9 @@ class SpecialUpload extends SpecialPage {
 
                // Success, redirect to description page
                $this->mUploadSuccessful = true;
-               Hooks::run( 'SpecialUploadComplete', [ &$this ] );
+               // Avoid PHP 7.1 warning of passing $this by reference
+               $upload = $this;
+               Hooks::run( 'SpecialUploadComplete', [ &$upload ] );
                $this->getOutput()->redirect( $this->mLocalFile->getTitle()->getFullURL() );
        }
 
index ea6ef30..96f8638 100644 (file)
@@ -775,7 +775,9 @@ abstract class UploadBase {
                                        User::IGNORE_USER_RIGHTS
                                );
                        }
-                       Hooks::run( 'UploadComplete', [ &$this ] );
+                       // Avoid PHP 7.1 warning of passing $this by reference
+                       $uploadBase = $this;
+                       Hooks::run( 'UploadComplete', [ &$uploadBase ] );
 
                        $this->postProcessUpload();
                }
index 505bc2d..b34a03f 100644 (file)
@@ -20617,16 +20617,59 @@ language=sr variant=sr-ec
 </p>
 !! end
 
-# FIXME: This test is currently broken in the PHP parser (bug 52661)
 !! test
-Don't break image parsing if language converter markup is in the caption.
+T146305: Don't break image parsing if language converter markup is in the caption.
 !! options
 language=sr
 !! wikitext
-[[File:Foobar.jpg|-{R|caption}-]]
+[[File:Foobar.jpg|thumb|-{R|caption:}-]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/%D0%94%D0%B0%D1%82%D0%BE%D1%82%D0%B5%D0%BA%D0%B0:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/%D0%94%D0%B0%D1%82%D0%BE%D1%82%D0%B5%D0%BA%D0%B0:Foobar.jpg" class="internal" title="Повећај"></a></div>caption:</div></div></div>
+
 !! html/parsoid
-<p><a href="/wiki/File:Foobar.jpg" class="image" title="caption"><img alt="caption" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="./Датотека:Foobar.jpg"><img resource="./Датотека:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><span typeof="mw:LanguageVariant" data-mw='{"disabled":true,"show":true,"text":"caption:"}'></span></figcaption></figure>
+!! end
+
+!! test
+T146305: Don't break image parsing if nested language converter markup is in the caption.
+!! options
+language=zh variant=zh-cn
+!! wikitext
+[[File:Foobar.jpg|thumb|-{zh-cn:blog (hk: -{zh-hans|WEBJOURNAL}-, tw: -{zh-hans|WEBLOG}-)}-]]
+!! html/php
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" class="thumbimage" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/270px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/360px-Foobar.jpg 2x" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="放大"></a></div>blog (hk: WEBJOURNAL, tw: WEBLOG)</div></div></div>
+
+!! html/parsoid
+<figure class="mw-default-size" typeof="mw:Image/Thumb"><a href="File:Foobar.jpg"><img resource="./File:Foobar.jpg" src="//example.com/images/thumb/3/3a/Foobar.jpg/220px-Foobar.jpg" data-file-width="1941" data-file-height="220" data-file-type="bitmap" height="25" width="220"/></a><figcaption><span typeof="mw:LanguageVariant" data-mw='{"bidir":[{"l":"zh-cn","t":"blog (hk: &lt;span typeof=\"mw:LanguageVariant\" data-parsoid=&#39;{\"fl\":[\"zh-hans\"],\"dsr\":[42,64,null,2]}&#39; data-mw=&#39;{\"filter\":[\"zh-hans\"],\"text\":\"WEBJOURNAL\"}&#39;>&lt;/span>, tw: &lt;span typeof=\"mw:LanguageVariant\" data-parsoid=&#39;{\"fl\":[\"zh-hans\"],\"dsr\":[70,88,null,2]}&#39; data-mw=&#39;{\"filter\":[\"zh-hans\"],\"text\":\"WEBLOG\"}&#39;>&lt;/span>)"}],"show":true}'></span></figcaption></figure>
+!! end
+
+!! test
+Don't break gallery if language converter markup is inside.
+!! options
+language=zh
+!! wikitext
+<gallery>
+File:foobar.jpg|[[File:foobar.jpg|20px|desc|alt=-{R|foo}-|-{R|bar}-]]|alt=-{R|bat}-
+File:foobar.jpg|{{Test|unamedParam|alt=-{R|param}-}}|alt=galleryalt
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+               <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+                       <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="bat" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+                       <div class="gallerytext">
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="bar"><img alt="foo" src="http://example.com/images/thumb/3/3a/Foobar.jpg/20px-Foobar.jpg" width="20" height="2" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/30px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/40px-Foobar.jpg 2x" /></a>
+</p>
+                       </div>
+               </div></li>
+               <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+                       <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+                       <div class="gallerytext">
+<p>This is a test template
 </p>
+                       </div>
+               </div></li>
+</ul>
+
 !! end
 
 # FIXME: This test is currently broken in the PHP parser (bug 52661)
@@ -22226,7 +22269,102 @@ Ignore pipe between table row attributes
 
 !! end
 
+!!test
+Gallery override link with WikiLink (bug 34852)
+!! wikitext
+<gallery>
+File:foobar.jpg|caption|alt=galleryalt|link=InterWikiLink
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+               <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+                       <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/InterWikiLink"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+                       <div class="gallerytext">
+<p>caption
+</p>
+                       </div>
+               </div></li>
+</ul>
+
+!! end
+
+!!test
+Gallery override link with absolute external link (bug 34852)
+!! wikitext
+<gallery>
+File:foobar.jpg|caption|alt=galleryalt|link=http://www.example.org
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+               <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+                       <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="http://www.example.org"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+                       <div class="gallerytext">
+<p>caption
+</p>
+                       </div>
+               </div></li>
+</ul>
+
+!! end
+
 !! test
+Gallery override link with absolute external link with LanguageConverter
+!! options
+language=zh
+!! input
+<gallery>
+File:foobar.jpg|caption|alt=galleryalt|link=http://www.example.org
+</gallery>
+!! result
+<ul class="gallery mw-gallery-traditional">
+               <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+                       <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="http://www.example.org"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+                       <div class="gallerytext">
+<p>caption
+</p>
+                       </div>
+               </div></li>
+</ul>
+
+!! end
+
+!!test
+Gallery override link with malicious javascript (bug 34852)
+!! wikitext
+<gallery>
+File:foobar.jpg|caption|alt=galleryalt|link=" onclick="alert('malicious javascript code!');
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+               <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+                       <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/%22_onclick%3D%22alert(%27malicious_javascript_code!%27);"><img alt="galleryalt" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+                       <div class="gallerytext">
+<p>caption
+</p>
+                       </div>
+               </div></li>
+</ul>
+
+!! end
+
+!!test
+Gallery with invalid title as link (bug 43964)
+!! wikitext
+<gallery>
+File:foobar.jpg|link=<
+</gallery>
+!! html
+<ul class="gallery mw-gallery-traditional">
+               <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+                       <div class="thumb" style="width: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg" width="120" height="14" srcset="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg 1.5x, http://example.com/images/thumb/3/3a/Foobar.jpg/240px-Foobar.jpg 2x" /></a></div></div>
+                       <div class="gallerytext">
+                       </div>
+               </div></li>
+</ul>
+
+!! end
+
+!!test
 Language parser function
 !! wikitext
 {{#language:ar}}