Fix localized setlang redirects for prefixed next paths
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user