From: Cormac Parle Date: Wed, 18 Oct 2017 12:38:48 +0000 (+0100) Subject: Treat langtags in SVG switch case-insensitively X-Git-Tag: 1.31.0-rc.0~1498^2 X-Git-Url: https://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/banques/?a=commitdiff_plain;h=f6620e2a7576b81ac5d3ca2f598746d1bc70c887;p=lhc%2Fweb%2Fwiklou.git Treat langtags in SVG switch case-insensitively See https://tools.ietf.org/html/bcp47#section-2.1.1 Also implement matching of systemLanguage attribs as specified in the SVG spec Note that librsvg that we use for rendering pngs of svg files has a bug, and matches languages in the following way instead of what is implemented in SVG::getMatchedLanguage() public function getMatchedLanguage( $userPreferredLanguage, array $svgLanguages ) { foreach ( $svgLanguages as $svgLang ) { if ($svgLang == $userPreferredLanguage) { return $svgLang; } $dashPosition = strpos( $userPreferredLanguage, '-' ); if ( $dashPosition !== false ) { if ( strtolower( substr( $svgLang, 0, $dashPosition ) ) == strtolower( substr( $userPreferredLanguage, 0, $dashPosition ) ) ) { return $svgLang; } } return null; } Bug: T154132 Change-Id: Ibff66a0844f0cecfae0260c6a7d20aeedc2849a2 --- diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php index 32f4504ba6..dd12ab5d95 100644 --- a/includes/filerepo/file/File.php +++ b/includes/filerepo/file/File.php @@ -581,6 +581,25 @@ abstract class File implements IDBAccessObject { } } + /** + * Get the language code from the available languages for this file that matches the language + * requested by the user + * + * @param string $userPreferredLanguage + * @return string|null + */ + public function getMatchedLanguage( $userPreferredLanguage ) { + $handler = $this->getHandler(); + if ( $handler && method_exists( $handler, 'getMatchedLanguage' ) ) { + return $handler->getMatchedLanguage( + $userPreferredLanguage, + $handler->getAvailableLanguages( $this ) + ); + } else { + return null; + } + } + /** * In files that support multiple language, what is the default language * to use if none specified. diff --git a/includes/media/SVG.php b/includes/media/SVG.php index bd78b49e5d..2b138930d1 100644 --- a/includes/media/SVG.php +++ b/includes/media/SVG.php @@ -97,19 +97,50 @@ class SvgHandler extends ImageHandler { if ( isset( $metadata['translations'] ) ) { foreach ( $metadata['translations'] as $lang => $langType ) { if ( $langType === SVGReader::LANG_FULL_MATCH ) { - $langList[] = $lang; + $langList[] = strtolower( $lang ); } } } } - return $langList; + return array_unique( $langList ); } /** - * What language to render file in if none selected. + * SVG's systemLanguage matching rules state: + * 'The `systemLanguage` attribute ... [e]valuates to "true" if one of the languages indicated + * by user preferences exactly equals one of the languages given in the value of this parameter, + * or if one of the languages indicated by user preferences exactly equals a prefix of one of + * the languages given in the value of this parameter such that the first tag character + * following the prefix is "-".' * - * @param File $file - * @return string Language code. + * Return the first element of $svgLanguages that matches $userPreferredLanguage + * + * @see https://www.w3.org/TR/SVG/struct.html#SystemLanguageAttribute + * @param string $userPreferredLanguage + * @param array $svgLanguages + * @return string|null + */ + public function getMatchedLanguage( $userPreferredLanguage, array $svgLanguages ) { + foreach ( $svgLanguages as $svgLang ) { + if ( strcasecmp( $svgLang, $userPreferredLanguage ) === 0 ) { + return $svgLang; + } + $trimmedSvgLang = $svgLang; + while ( strpos( $trimmedSvgLang, '-' ) !== false ) { + $trimmedSvgLang = substr( $trimmedSvgLang, 0, strrpos( $trimmedSvgLang, '-' ) ); + if ( strcasecmp( $trimmedSvgLang, $userPreferredLanguage ) === 0 ) { + return $svgLang; + } + } + } + return null; + } + + /** + * What language to render file in if none selected + * + * @param File $file Language code + * @return string */ public function getDefaultRenderLanguage( File $file ) { return 'en'; @@ -479,7 +510,7 @@ class SvgHandler extends ImageHandler { return ( $value > 0 ); } elseif ( $name == 'lang' ) { // Validate $code - if ( $value === '' || !Language::isValidBuiltInCode( $value ) ) { + if ( $value === '' || !Language::isValidCode( $value ) ) { wfDebug( "Invalid user language code\n" ); return false; @@ -499,8 +530,7 @@ class SvgHandler extends ImageHandler { public function makeParamString( $params ) { $lang = ''; if ( isset( $params['lang'] ) && $params['lang'] !== 'en' ) { - $params['lang'] = strtolower( $params['lang'] ); - $lang = "lang{$params['lang']}-"; + $lang = 'lang' . strtolower( $params['lang'] ) . '-'; } if ( !isset( $params['width'] ) ) { return false; @@ -511,7 +541,7 @@ class SvgHandler extends ImageHandler { public function parseParamString( $str ) { $m = false; - if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/', $str, $m ) ) { + if ( preg_match( '/^lang([a-z]+(?:-[a-z]+)*)-(\d+)px$/i', $str, $m ) ) { return [ 'width' => array_pop( $m ), 'lang' => $m[1] ]; } elseif ( preg_match( '/^(\d+)px$/', $str, $m ) ) { return [ 'width' => $m[1], 'lang' => 'en' ]; diff --git a/includes/page/ImagePage.php b/includes/page/ImagePage.php index 62f5d00c33..935d299fd3 100644 --- a/includes/page/ImagePage.php +++ b/includes/page/ImagePage.php @@ -285,6 +285,19 @@ class ImagePage extends Article { return parent::getContentObject(); } + private function getLanguageForRendering( WebRequest $request, File $file ) { + $handler = $this->displayImg->getHandler(); + + $requestLanguage = $request->getVal( 'lang' ); + if ( !is_null( $requestLanguage ) ) { + if ( $handler && $handler->validateParam( 'lang', $requestLanguage ) ) { + return $requestLanguage; + } + } + + return $handler->getDefaultRenderLanguage( $this->displayImg ); + } + protected function openShowImage() { global $wgEnableUploads, $wgSend404Code, $wgSVGMaxSize; @@ -309,14 +322,9 @@ class ImagePage extends Article { $params = [ 'page' => $page ]; } - $renderLang = $request->getVal( 'lang' ); + $renderLang = $this->getLanguageForRendering( $request, $this->displayImg ); if ( !is_null( $renderLang ) ) { - $handler = $this->displayImg->getHandler(); - if ( $handler && $handler->validateParam( 'lang', $renderLang ) ) { - $params['lang'] = $renderLang; - } else { - $renderLang = null; - } + $params['lang'] = $renderLang; } $width_orig = $this->displayImg->getWidth( $page ); @@ -544,12 +552,7 @@ EOT $renderLangOptions = $this->displayImg->getAvailableLanguages(); if ( count( $renderLangOptions ) >= 1 ) { - $currentLanguage = $renderLang; - $defaultLang = $this->displayImg->getDefaultRenderLanguage(); - if ( is_null( $currentLanguage ) ) { - $currentLanguage = $defaultLang; - } - $out->addHTML( $this->doRenderLangOpt( $renderLangOptions, $currentLanguage, $defaultLang ) ); + $out->addHTML( $this->doRenderLangOpt( $renderLangOptions, $renderLang ) ); } // Add cannot animate thumbnail warning @@ -1047,60 +1050,31 @@ EOT * Output a drop-down box for language options for the file * * @param array $langChoices Array of string language codes - * @param string $curLang Language code file is being viewed in. - * @param string $defaultLang Language code that image is rendered in by default + * @param string $renderLang Language code for the language we want the file to rendered in. * @return string HTML to insert underneath image. */ - protected function doRenderLangOpt( array $langChoices, $curLang, $defaultLang ) { + protected function doRenderLangOpt( array $langChoices, $renderLang ) { global $wgScript; - sort( $langChoices ); - $curLang = LanguageCode::bcp47( $curLang ); - $defaultLang = LanguageCode::bcp47( $defaultLang ); $opts = ''; - $haveCurrentLang = false; - $haveDefaultLang = false; - - // We make a list of all the language choices in the file. - // Additionally if the default language to render this file - // is not included as being in this file (for example, in svgs - // usually the fallback content is the english content) also - // include a choice for that. Last of all, if we're viewing - // the file in a language not on the list, add it as a choice. + + $matchedRenderLang = $this->displayImg->getMatchedLanguage( $renderLang ); + foreach ( $langChoices as $lang ) { - $code = LanguageCode::bcp47( $lang ); - $name = Language::fetchLanguageName( $code, $this->getContext()->getLanguage()->getCode() ); - if ( $name !== '' ) { - $display = $this->getContext()->msg( 'img-lang-opt', $code, $name )->text(); - } else { - $display = $code; - } - $opts .= "\n" . Xml::option( $display, $code, $curLang === $code ); - if ( $curLang === $code ) { - $haveCurrentLang = true; - } - if ( $defaultLang === $code ) { - $haveDefaultLang = true; - } - } - if ( !$haveDefaultLang ) { - // Its hard to know if the content is really in the default language, or - // if its just unmarked content that could be in any language. - $opts = Xml::option( - $this->getContext()->msg( 'img-lang-default' )->text(), - $defaultLang, - $defaultLang === $curLang - ) . $opts; - } - if ( !$haveCurrentLang && $defaultLang !== $curLang ) { - $name = Language::fetchLanguageName( $curLang, $this->getContext()->getLanguage()->getCode() ); - if ( $name !== '' ) { - $display = $this->getContext()->msg( 'img-lang-opt', $curLang, $name )->text(); - } else { - $display = $curLang; - } - $opts = Xml::option( $display, $curLang, true ) . $opts; + $opts .= $this->createXmlOptionStringForLanguage( + $lang, + $matchedRenderLang === $lang + ); } + // Allow for the default case in an svg that is displayed if no + // systemLanguage attribute matches + $opts .= "\n" . + Xml::option( + $this->getContext()->msg( 'img-lang-default' )->text(), + 'und', + is_null( $matchedRenderLang ) + ); + $select = Html::rawElement( 'select', [ 'id' => 'mw-imglangselector', 'name' => 'lang' ], @@ -1119,6 +1093,27 @@ EOT return $langSelectLine; } + /** + * @param $lang string + * @param $selected bool + * @return string + */ + private function createXmlOptionStringForLanguage( $lang, $selected ) { + $code = LanguageCode::bcp47( $lang ); + $name = Language::fetchLanguageName( $code, $this->getContext()->getLanguage()->getCode() ); + if ( $name !== '' ) { + $display = $this->getContext()->msg( 'img-lang-opt', $code, $name )->text(); + } else { + $display = $code; + } + return "\n" . + Xml::option( + $display, + $lang, + $selected + ); + } + /** * Get the width and height to display image at. * diff --git a/tests/parser/parserTests.txt b/tests/parser/parserTests.txt index 3c861ea10b..41b09879e0 100644 --- a/tests/parser/parserTests.txt +++ b/tests/parser/parserTests.txt @@ -15046,9 +15046,9 @@ SVG thumbnails with invalid language code !! options parsoid=wt2html,wt2wt,html2html !! wikitext -[[File:Foobar.svg|thumb|caption|lang=invalid.language.code]] +[[File:Foobar.svg|thumb|caption|lang=invalid:language:code]] !! html/php -
lang=invalid.language.code
+
lang=invalid:language:code
!! html/parsoid
lang=invalid.language.code
diff --git a/tests/phpunit/includes/media/SVGTest.php b/tests/phpunit/includes/media/SVGTest.php index 4a986b4cc1..9fd640f56e 100644 --- a/tests/phpunit/includes/media/SVGTest.php +++ b/tests/phpunit/includes/media/SVGTest.php @@ -5,6 +5,11 @@ */ class SvgTest extends MediaWikiMediaTestCase { + /** + * @var SvgHandler + */ + private $handler; + protected function setUp() { parent::setUp(); @@ -38,4 +43,71 @@ class SvgTest extends MediaWikiMediaTestCase { [ 'Wikimedia-logo.svg', [] ] ]; } + + /** + * @param string $userPreferredLanguage + * @param array $svgLanguages + * @param string $expectedMatch + * @dataProvider providerGetMatchedLanguage + * @covers SvgHandler::getMatchedLanguage + */ + public function testGetMatchedLanguage( $userPreferredLanguage, $svgLanguages, $expectedMatch ) { + $match = $this->handler->getMatchedLanguage( $userPreferredLanguage, $svgLanguages ); + $this->assertEquals( $expectedMatch, $match ); + } + + public function providerGetMatchedLanguage() { + return [ + 'no match' => [ + 'userPreferredLanguage' => 'en', + 'svgLanguages' => [ 'de-DE', 'zh', 'ga', 'fr', 'sr-Latn-ME' ], + 'expectedMatch' => null, + ], + 'no subtags' => [ + 'userPreferredLanguage' => 'en', + 'svgLanguages' => [ 'de', 'zh', 'en', 'fr' ], + 'expectedMatch' => 'en', + ], + 'user no subtags, svg 1 subtag' => [ + 'userPreferredLanguage' => 'en', + 'svgLanguages' => [ 'de-DE', 'en-GB', 'en-US', 'fr' ], + 'expectedMatch' => 'en-GB', + ], + 'user no subtags, svg >1 subtag' => [ + 'userPreferredLanguage' => 'sr', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'en-US', 'fr' ], + 'expectedMatch' => 'sr-Cyrl-BA', + ], + 'user 1 subtag, svg no subtags' => [ + 'userPreferredLanguage' => 'en-US', + 'svgLanguages' => [ 'de', 'en', 'en', 'fr' ], + 'expectedMatch' => null, + ], + 'user 1 subtag, svg 1 subtag' => [ + 'userPreferredLanguage' => 'en-US', + 'svgLanguages' => [ 'de-DE', 'en-GB', 'en-US', 'fr' ], + 'expectedMatch' => 'en-US', + ], + 'user 1 subtag, svg >1 subtag' => [ + 'userPreferredLanguage' => 'sr-Latn', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'fr' ], + 'expectedMatch' => 'sr-Latn-ME', + ], + 'user >1 subtag, svg >1 subtag' => [ + 'userPreferredLanguage' => 'sr-Latn-ME', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl-BA', 'sr-Latn-ME', 'en-US', 'fr' ], + 'expectedMatch' => 'sr-Latn-ME', + ], + 'user >1 subtag, svg <=1 subtag' => [ + 'userPreferredLanguage' => 'sr-Latn-ME', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl', 'sr-Latn', 'en-US', 'fr' ], + 'expectedMatch' => null, + ], + 'ensure case-insensitive' => [ + 'userPreferredLanguage' => 'sr-latn', + 'svgLanguages' => [ 'de-DE', 'sr-Cyrl', 'sr-Latn-ME', 'en-US', 'fr' ], + 'expectedMatch' => 'sr-Latn-ME', + ], + ]; + } }