fix(i18n): normalize setlang next path server-side
This commit is contained in:
31
mandelstudio/i18n_utils.py
Normal file
31
mandelstudio/i18n_utils.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def normalize_set_language_next(value: str | None) -> str:
|
||||
"""
|
||||
Normalize the `next` path used by Django's set_language view.
|
||||
|
||||
Removes any leading language prefix from the path so switching from one
|
||||
locale to another cannot produce duplicated prefixes like `/de/en/...`.
|
||||
"""
|
||||
if not value:
|
||||
return "/"
|
||||
|
||||
parsed = urlsplit(str(value))
|
||||
path = parsed.path or "/"
|
||||
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 "/"
|
||||
|
||||
return urlunsplit(("", "", path, parsed.query, ""))
|
||||
24
mandelstudio/i18n_views.py
Normal file
24
mandelstudio/i18n_views.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.views.i18n import set_language as django_set_language
|
||||
|
||||
from .i18n_utils import normalize_set_language_next
|
||||
|
||||
|
||||
def set_language_normalized(request: HttpRequest) -> HttpResponse:
|
||||
"""Normalize `next` before delegating to Django's set_language view."""
|
||||
if request.method == "POST":
|
||||
next_value = request.POST.get("next")
|
||||
if next_value is not None:
|
||||
post_data = request.POST.copy()
|
||||
post_data["next"] = normalize_set_language_next(next_value)
|
||||
request.POST = post_data
|
||||
else:
|
||||
next_value = request.GET.get("next")
|
||||
if next_value is not None:
|
||||
get_data = request.GET.copy()
|
||||
get_data["next"] = normalize_set_language_next(next_value)
|
||||
request.GET = get_data
|
||||
|
||||
return django_set_language(request)
|
||||
@@ -1,10 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
|
||||
from mandelstudio.i18n_utils import normalize_set_language_next
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -30,20 +28,4 @@ def skip_to_content_text(context) -> str:
|
||||
@register.filter(name="language_neutral_path")
|
||||
def language_neutral_path(value: str | None) -> str:
|
||||
"""Normalize a path for set_language by removing any leading language prefix."""
|
||||
if not value:
|
||||
return "/"
|
||||
|
||||
parsed = urlsplit(str(value))
|
||||
path = parsed.path or "/"
|
||||
if not path.startswith("/"):
|
||||
path = f"/{path}"
|
||||
|
||||
language_codes = [code for code, _ in settings.LANGUAGES]
|
||||
if language_codes:
|
||||
# Strip the first language segment, e.g. /en/manage -> /manage.
|
||||
pattern = (
|
||||
rf"^/(?:{'|'.join(re.escape(code) for code in language_codes)})(?=/|$)"
|
||||
)
|
||||
path = re.sub(pattern, "", path, count=1) or "/"
|
||||
|
||||
return urlunsplit(("", "", path, parsed.query, ""))
|
||||
return normalize_set_language_next(value)
|
||||
|
||||
63
mandelstudio/tests/test_i18n_set_language_view.py
Normal file
63
mandelstudio/tests/test_i18n_set_language_view.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.test import RequestFactory, SimpleTestCase, override_settings
|
||||
|
||||
from mandelstudio.i18n_utils import normalize_set_language_next
|
||||
from mandelstudio.i18n_views import set_language_normalized
|
||||
|
||||
|
||||
@override_settings(
|
||||
LANGUAGES=(
|
||||
("nl", "Dutch"),
|
||||
("en", "English"),
|
||||
("de", "German"),
|
||||
("fr", "French"),
|
||||
("es", "Spanish"),
|
||||
("it", "Italian"),
|
||||
("pt", "Portuguese"),
|
||||
("ru", "Russian"),
|
||||
)
|
||||
)
|
||||
class SetLanguageNormalizationTests(SimpleTestCase):
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_normalize_set_language_next_strips_single_prefix(self):
|
||||
self.assertEqual(
|
||||
normalize_set_language_next("/en/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(
|
||||
request.POST["next"]
|
||||
)
|
||||
request = self.factory.post(
|
||||
"/i18n/setlang/",
|
||||
data={"language": "de", "next": "/en/manage/checkout/paymentmethod/"},
|
||||
)
|
||||
|
||||
response = set_language_normalized(request)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(
|
||||
django_set_language.call_args.args[0].POST["next"],
|
||||
"/manage/checkout/paymentmethod/",
|
||||
)
|
||||
|
||||
@patch("mandelstudio.i18n_views.django_set_language")
|
||||
def test_get_next_is_normalized_before_delegate(self, django_set_language):
|
||||
django_set_language.side_effect = lambda request: HttpResponseRedirect(
|
||||
request.GET["next"]
|
||||
)
|
||||
request = self.factory.get(
|
||||
"/i18n/setlang/",
|
||||
data={"language": "fr", "next": "/de/manage/"},
|
||||
)
|
||||
|
||||
response = set_language_normalized(request)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(django_set_language.call_args.args[0].GET["next"], "/manage/")
|
||||
@@ -1,13 +1,14 @@
|
||||
from django.urls import include, path
|
||||
from django.urls import path
|
||||
from django.views.decorators.cache import cache_page
|
||||
|
||||
from ocyan.main.urls import urlpatterns as ocyan_urlpatterns
|
||||
from ocyan.plugin.wagtail_oscar_integration.constants import CACHE_DURATION
|
||||
|
||||
from .i18n_views import set_language_normalized
|
||||
from .sitemaps import robots_txt, sitemap_index, sitemap_section
|
||||
|
||||
urlpatterns = [
|
||||
path("i18n/", include("django.conf.urls.i18n")),
|
||||
path("i18n/setlang/", set_language_normalized, name="set_language"),
|
||||
path("robots.txt", robots_txt, name="robots-txt"),
|
||||
path(
|
||||
"sitemap.xml",
|
||||
|
||||
Reference in New Issue
Block a user