diff --git a/mandelstudio/i18n_views.py b/mandelstudio/i18n_views.py index f3b9de0..475d99a 100644 --- a/mandelstudio/i18n_views.py +++ b/mandelstudio/i18n_views.py @@ -1,28 +1,55 @@ from __future__ import annotations -import inspect - -from django.http import HttpRequest, HttpResponse +from django.conf import settings +from django.http import HttpRequest, HttpResponse, HttpResponseRedirect +from django.utils.http import url_has_allowed_host_and_scheme +from django.utils.translation import check_for_language +from django.utils.translation import get_language as _get_language +from django.utils.translation import translate_url +from django.views.i18n import LANGUAGE_QUERY_PARAMETER 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": - 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 + """ + Set language while normalizing `next` to avoid duplicated locale prefixes. - return inspect.unwrap(django_set_language)(request) + Mirrors Django's set_language behavior closely, but enforces `next` + normalization before translating redirects. + """ + next_url = request.POST.get("next", request.GET.get("next")) + if next_url: + next_url = normalize_set_language_next(next_url) + if next_url and not url_has_allowed_host_and_scheme( + url=next_url, + allowed_hosts={request.get_host()}, + require_https=request.is_secure(), + ): + next_url = request.META.get("HTTP_REFERER") + if not next_url: + next_url = "/" + + response: HttpResponse = HttpResponseRedirect(next_url) + + if request.method == "POST": + lang_code = request.POST.get(LANGUAGE_QUERY_PARAMETER) + if lang_code and check_for_language(lang_code): + translated = translate_url(next_url, lang_code) + if translated != next_url: + response = HttpResponseRedirect(translated) + response.set_cookie( + settings.LANGUAGE_COOKIE_NAME, + lang_code, + max_age=settings.LANGUAGE_COOKIE_AGE, + path=settings.LANGUAGE_COOKIE_PATH, + domain=settings.LANGUAGE_COOKIE_DOMAIN, + secure=settings.LANGUAGE_COOKIE_SECURE, + httponly=settings.LANGUAGE_COOKIE_HTTPONLY, + samesite=settings.LANGUAGE_COOKIE_SAMESITE, + ) + + response.headers.setdefault("Content-Language", _get_language()) + return response diff --git a/mandelstudio/tests/test_i18n_set_language_view.py b/mandelstudio/tests/test_i18n_set_language_view.py index 0ecd362..076b5e2 100644 --- a/mandelstudio/tests/test_i18n_set_language_view.py +++ b/mandelstudio/tests/test_i18n_set_language_view.py @@ -1,6 +1,3 @@ -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 @@ -35,47 +32,40 @@ class SetLanguageNormalizationTests(SimpleTestCase): "/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"] - ) + def test_post_next_is_normalized_before_delegate(self): request = self.factory.post( "/i18n/setlang/", data={"language": "de", "next": "/en/manage/checkout/paymentmethod/"}, ) + request.META["HTTP_HOST"] = "testserver" 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/", + response["Location"], + "/de/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"] - ) + def test_get_next_is_normalized_before_delegate(self): request = self.factory.get( "/i18n/setlang/", data={"language": "fr", "next": "/de/manage/"}, ) + request.META["HTTP_HOST"] = "testserver" response = set_language_normalized(request) self.assertEqual(response.status_code, 302) - self.assertEqual(django_set_language.call_args.args[0].GET["next"], "/manage/") + self.assertEqual(response["Location"], "/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/") + def test_set_language_view_is_csrf_exempt(self): request = self.factory.post( "/i18n/setlang/", data={"language": "nl", "next": "/en/manage/"}, ) request.csrf_processing_done = False + request.META["HTTP_HOST"] = "testserver" response = set_language_normalized(request)