Fix localized setlang redirects for prefixed next paths
This commit is contained in:
@@ -1,28 +1,55 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import inspect
|
from django.conf import settings
|
||||||
|
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||||
from django.http import HttpRequest, HttpResponse
|
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.decorators.csrf import csrf_exempt
|
||||||
from django.views.i18n import set_language as django_set_language
|
|
||||||
|
|
||||||
from .i18n_utils import normalize_set_language_next
|
from .i18n_utils import normalize_set_language_next
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def set_language_normalized(request: HttpRequest) -> HttpResponse:
|
def set_language_normalized(request: HttpRequest) -> HttpResponse:
|
||||||
"""Normalize `next` before delegating to Django's set_language view."""
|
"""
|
||||||
if request.method == "POST":
|
Set language while normalizing `next` to avoid duplicated locale prefixes.
|
||||||
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 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 django.test import RequestFactory, SimpleTestCase, override_settings
|
||||||
|
|
||||||
from mandelstudio.i18n_utils import normalize_set_language_next
|
from mandelstudio.i18n_utils import normalize_set_language_next
|
||||||
@@ -35,47 +32,40 @@ class SetLanguageNormalizationTests(SimpleTestCase):
|
|||||||
"/manage/checkout/paymentmethod/",
|
"/manage/checkout/paymentmethod/",
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("mandelstudio.i18n_views.django_set_language")
|
def test_post_next_is_normalized_before_delegate(self):
|
||||||
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(
|
request = self.factory.post(
|
||||||
"/i18n/setlang/",
|
"/i18n/setlang/",
|
||||||
data={"language": "de", "next": "/en/manage/checkout/paymentmethod/"},
|
data={"language": "de", "next": "/en/manage/checkout/paymentmethod/"},
|
||||||
)
|
)
|
||||||
|
request.META["HTTP_HOST"] = "testserver"
|
||||||
|
|
||||||
response = set_language_normalized(request)
|
response = set_language_normalized(request)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
django_set_language.call_args.args[0].POST["next"],
|
response["Location"],
|
||||||
"/manage/checkout/paymentmethod/",
|
"/de/manage/checkout/paymentmethod/",
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("mandelstudio.i18n_views.django_set_language")
|
def test_get_next_is_normalized_before_delegate(self):
|
||||||
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(
|
request = self.factory.get(
|
||||||
"/i18n/setlang/",
|
"/i18n/setlang/",
|
||||||
data={"language": "fr", "next": "/de/manage/"},
|
data={"language": "fr", "next": "/de/manage/"},
|
||||||
)
|
)
|
||||||
|
request.META["HTTP_HOST"] = "testserver"
|
||||||
|
|
||||||
response = set_language_normalized(request)
|
response = set_language_normalized(request)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 302)
|
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):
|
||||||
def test_set_language_view_is_csrf_exempt(self, django_set_language):
|
|
||||||
django_set_language.return_value = HttpResponseRedirect("/manage/")
|
|
||||||
request = self.factory.post(
|
request = self.factory.post(
|
||||||
"/i18n/setlang/",
|
"/i18n/setlang/",
|
||||||
data={"language": "nl", "next": "/en/manage/"},
|
data={"language": "nl", "next": "/en/manage/"},
|
||||||
)
|
)
|
||||||
request.csrf_processing_done = False
|
request.csrf_processing_done = False
|
||||||
|
request.META["HTTP_HOST"] = "testserver"
|
||||||
|
|
||||||
response = set_language_normalized(request)
|
response = set_language_normalized(request)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user