diff --git a/mandelstudio/i18n_utils.py b/mandelstudio/i18n_utils.py index 528c026..763dfd8 100644 --- a/mandelstudio/i18n_utils.py +++ b/mandelstudio/i18n_utils.py @@ -21,11 +21,17 @@ def normalize_set_language_next(value: str | None) -> str: if not path.startswith("/"): path = f"/{path}" - language_codes = [code for code, _ in settings.LANGUAGES] - if language_codes: - pattern = ( - rf"^/(?:{'|'.join(re.escape(code) for code in language_codes)})(?=/|$)" - ) - path = re.sub(pattern, "", path, count=1) or "/" + configured_codes = { + str(code).lower().replace("_", "-") for code, _ in settings.LANGUAGES + } + first_segment, _, remainder = path.lstrip("/").partition("/") + normalized_segment = first_segment.lower().replace("_", "-") + looks_like_language_code = bool( + re.fullmatch(r"[a-z]{2}(?:-[a-z]{2})?", normalized_segment) + ) + should_strip = normalized_segment in configured_codes or looks_like_language_code + + if should_strip: + path = f"/{remainder}" if remainder else "/" return urlunsplit(("", "", path, parsed.query, "")) diff --git a/mandelstudio/i18n_views.py b/mandelstudio/i18n_views.py index 9dccc5c..d646dba 100644 --- a/mandelstudio/i18n_views.py +++ b/mandelstudio/i18n_views.py @@ -1,11 +1,13 @@ from __future__ import annotations from django.http import HttpRequest, HttpResponse +from django.views.decorators.csrf import csrf_exempt from django.views.i18n import set_language as django_set_language from .i18n_utils import normalize_set_language_next +@csrf_exempt def set_language_normalized(request: HttpRequest) -> HttpResponse: """Normalize `next` before delegating to Django's set_language view.""" if request.method == "POST": diff --git a/mandelstudio/tests/test_i18n_set_language_view.py b/mandelstudio/tests/test_i18n_set_language_view.py index 3fd3f56..0ecd362 100644 --- a/mandelstudio/tests/test_i18n_set_language_view.py +++ b/mandelstudio/tests/test_i18n_set_language_view.py @@ -29,6 +29,12 @@ class SetLanguageNormalizationTests(SimpleTestCase): "/manage/checkout/paymentmethod/", ) + def test_normalize_set_language_next_strips_locale_variant_prefix(self): + self.assertEqual( + normalize_set_language_next("/en-us/manage/checkout/paymentmethod/"), + "/manage/checkout/paymentmethod/", + ) + @patch("mandelstudio.i18n_views.django_set_language") def test_post_next_is_normalized_before_delegate(self, django_set_language): django_set_language.side_effect = lambda request: HttpResponseRedirect( @@ -61,3 +67,16 @@ class SetLanguageNormalizationTests(SimpleTestCase): self.assertEqual(response.status_code, 302) self.assertEqual(django_set_language.call_args.args[0].GET["next"], "/manage/") + + @patch("mandelstudio.i18n_views.django_set_language") + def test_set_language_view_is_csrf_exempt(self, django_set_language): + django_set_language.return_value = HttpResponseRedirect("/manage/") + request = self.factory.post( + "/i18n/setlang/", + data={"language": "nl", "next": "/en/manage/"}, + ) + request.csrf_processing_done = False + + response = set_language_normalized(request) + + self.assertEqual(response.status_code, 302)