diff --git a/superset/views/core.py b/superset/views/core.py index b58d3520f94..88bd86f16f2 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -130,6 +130,11 @@ PARAMETER_MISSING_ERR = __( SqlResults = dict[str, Any] +# Matches the locale codes Superset actually ships: a 2-3 letter language +# subtag, optionally followed by a 2-letter region subtag ("pt_BR") or a +# 4-letter script subtag ("sr_Latn"). +LANGUAGE_CODE_RE = re.compile(r"^[a-z]{2,3}(_[A-Z]{2}|_[A-Z][a-z]{3})?$") + class Superset(BaseSupersetView): """The base views for Superset!""" @@ -920,7 +925,7 @@ class Superset(BaseSupersetView): @expose("/language_pack//") def language_pack(self, lang: str) -> FlaskResponse: # Only allow expected language formats like "en", "pt_BR", etc. - if not re.match(r"^[a-z]{2,3}(_[A-Z]{2})?$", lang): + if not LANGUAGE_CODE_RE.match(lang): abort(400, "Invalid language code") base_dir = os.path.join(os.path.dirname(__file__), "..", "translations") @@ -947,7 +952,7 @@ class Superset(BaseSupersetView): and must load for anonymous principals (login page, embedded). """ # Only allow expected language formats like "en", "pt_BR", etc. - if not re.match(r"^[a-z]{2,3}(_[A-Z]{2})?$", lang): + if not LANGUAGE_CODE_RE.match(lang): abort(400, "Invalid language code") if not re.match(r"^[0-9a-f]{12}$", version): abort(400, "Invalid language pack version") diff --git a/tests/unit_tests/views/test_bootstrap_auth.py b/tests/unit_tests/views/test_bootstrap_auth.py index 269ece0aff1..cbce9e628fe 100644 --- a/tests/unit_tests/views/test_bootstrap_auth.py +++ b/tests/unit_tests/views/test_bootstrap_auth.py @@ -218,7 +218,9 @@ def test_common_bootstrap_payload_does_not_mutate_memoized_dict( assert "language_pack" not in cached -def _language_pack_context(locale: str, payload_extra: dict[str, Any]) -> Any: +def _language_pack_context( + locale: str, payload_extra: dict[str, Any] +) -> dict[str, Any]: from superset.views.base import get_language_pack_template_context with ( diff --git a/tests/unit_tests/views/test_language_pack_script.py b/tests/unit_tests/views/test_language_pack_script.py index 453e80c3f7e..76ae54d8129 100644 --- a/tests/unit_tests/views/test_language_pack_script.py +++ b/tests/unit_tests/views/test_language_pack_script.py @@ -78,6 +78,21 @@ def test_script_rejects_malformed_lang_and_version(client: Any) -> None: assert client.get("/language_pack/fr/not-a-hash!/script.js").status_code == 400 +def test_script_accepts_script_subtag_locale(client: Any) -> None: + """Script-subtag locales Superset ships (e.g. sr_Latn) must not be + rejected by the language-code validation.""" + with ( + patch( + "superset.views.core.get_language_pack_version", + return_value=FAKE_VERSION, + ), + patch("superset.views.core.get_language_pack", return_value=FAKE_PACK), + ): + response = client.get(f"/language_pack/sr_Latn/{FAKE_VERSION}/script.js") + + assert response.status_code == 200 + + def test_script_serves_only_the_requested_locale(client: Any) -> None: """The endpoint resolves exactly one pack: the locale in the URL.""" with (