Skip to content

Commit 940b7fd

Browse files
KrzysiekJclaudep
authored andcommitted
Fixed #21446 -- Allowed not performing redirect in set_language view
Thanks Claude Paroz and Tim Graham for polishing the patch.
1 parent 12ba20d commit 940b7fd

File tree

4 files changed

+106
-20
lines changed

4 files changed

+106
-20
lines changed

django/views/i18n.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,18 @@ def set_language(request):
3434
any state.
3535
"""
3636
next = request.POST.get('next', request.GET.get('next'))
37-
if not is_safe_url(url=next, host=request.get_host()):
37+
if (next or not request.is_ajax()) and not is_safe_url(url=next, host=request.get_host()):
3838
next = request.META.get('HTTP_REFERER')
3939
if not is_safe_url(url=next, host=request.get_host()):
4040
next = '/'
41-
response = http.HttpResponseRedirect(next)
41+
response = http.HttpResponseRedirect(next) if next else http.HttpResponse(status=204)
4242
if request.method == 'POST':
4343
lang_code = request.POST.get(LANGUAGE_QUERY_PARAMETER)
4444
if lang_code and check_for_language(lang_code):
45-
next_trans = translate_url(next, lang_code)
46-
if next_trans != next:
47-
response = http.HttpResponseRedirect(next_trans)
45+
if next:
46+
next_trans = translate_url(next, lang_code)
47+
if next_trans != next:
48+
response = http.HttpResponseRedirect(next_trans)
4849
if hasattr(request, 'session'):
4950
request.session[LANGUAGE_SESSION_KEY] = lang_code
5051
else:

docs/releases/1.10.txt

+7
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,10 @@ Internationalization
267267
:func:`~django.conf.urls.i18n.i18n_patterns` to ``False``, you can allow
268268
accessing the default language without a URL prefix.
269269

270+
* :func:`~django.views.i18n.set_language` now returns a 204 status code (No
271+
Content) for AJAX requests when there is no ``next`` parameter in ``POST`` or
272+
``GET``.
273+
270274
Management Commands
271275
~~~~~~~~~~~~~~~~~~~
272276

@@ -695,6 +699,9 @@ Miscellaneous
695699
:meth:`~django.test.Client.login()` method no longer always rejects inactive
696700
users but instead delegates this decision to the authentication backend.
697701

702+
* :func:`django.views.i18n.set_language` may now return a 204 status code for
703+
AJAX requests.
704+
698705
.. _deprecated-features-1.10:
699706

700707
Features deprecated in 1.10

docs/topics/i18n/translation.txt

+15-8
Original file line numberDiff line numberDiff line change
@@ -1788,14 +1788,21 @@ saves the language choice in the user's session. Otherwise, it saves the
17881788
language choice in a cookie that is by default named ``django_language``.
17891789
(The name can be changed through the :setting:`LANGUAGE_COOKIE_NAME` setting.)
17901790

1791-
After setting the language choice, Django redirects the user, following this
1792-
algorithm:
1793-
1794-
* Django looks for a ``next`` parameter in the ``POST`` data.
1795-
* If that doesn't exist, or is empty, Django tries the URL in the
1796-
``Referrer`` header.
1797-
* If that's empty -- say, if a user's browser suppresses that header --
1798-
then the user will be redirected to ``/`` (the site root) as a fallback.
1791+
After setting the language choice, Django looks for a ``next`` parameter in the
1792+
``POST`` or ``GET`` data. If that is found and Django considers it to be a safe
1793+
URL (i.e. it doesn't point to a different host and uses a safe scheme), a
1794+
redirect to that URL will be performed. Otherwise, Django may fall back to
1795+
redirecting the user to the URL from the ``Referer`` header or, if it is not
1796+
set, to ``/``, depending on the nature of the request:
1797+
1798+
* For AJAX requests, the fallback will be performed only if the ``next``
1799+
parameter was set. Otherwise a 204 status code (No Content) will be returned.
1800+
* For non-AJAX requests, the fallback will always be performed.
1801+
1802+
.. versionchanged:: 1.10
1803+
1804+
Returning a 204 status code for AJAX requests when no redirect is specified
1805+
is new.
17991806

18001807
Here's example HTML template code:
18011808

tests/view_tests/tests/test_i18n.py

+78-7
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
from django.urls import reverse
1414
from django.utils import six
1515
from django.utils._os import upath
16-
from django.utils.translation import LANGUAGE_SESSION_KEY, override
16+
from django.utils.translation import (
17+
LANGUAGE_SESSION_KEY, get_language, override,
18+
)
1719

1820
from ..urls import locale_dir
1921

@@ -22,29 +24,98 @@
2224
class I18NTests(TestCase):
2325
""" Tests django views in django/views/i18n.py """
2426

27+
def _get_inactive_language_code(self):
28+
"""Return language code for a language which is not activated."""
29+
current_language = get_language()
30+
return [code for code, name in settings.LANGUAGES if not code == current_language][0]
31+
2532
def test_setlang(self):
2633
"""
2734
The set_language view can be used to change the session language.
2835
2936
The user is redirected to the 'next' argument if provided.
3037
"""
31-
for lang_code, lang_name in settings.LANGUAGES:
32-
post_data = dict(language=lang_code, next='/')
33-
response = self.client.post('/i18n/setlang/', data=post_data)
34-
self.assertRedirects(response, '/')
35-
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
38+
lang_code = self._get_inactive_language_code()
39+
post_data = dict(language=lang_code, next='/')
40+
response = self.client.post('/i18n/setlang/', post_data, HTTP_REFERER='/i_should_not_be_used/')
41+
self.assertRedirects(response, '/')
42+
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
3643

3744
def test_setlang_unsafe_next(self):
3845
"""
3946
The set_language view only redirects to the 'next' argument if it is
4047
"safe".
4148
"""
42-
lang_code, lang_name = settings.LANGUAGES[0]
49+
lang_code = self._get_inactive_language_code()
4350
post_data = dict(language=lang_code, next='//unsafe/redirection/')
4451
response = self.client.post('/i18n/setlang/', data=post_data)
4552
self.assertEqual(response.url, '/')
4653
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
4754

55+
def test_setlang_redirect_to_referer(self):
56+
"""
57+
The set_language view redirects to the URL in the referer header when
58+
there isn't a "next" parameter.
59+
"""
60+
lang_code = self._get_inactive_language_code()
61+
post_data = dict(language=lang_code)
62+
response = self.client.post('/i18n/setlang/', post_data, HTTP_REFERER='/i18n/')
63+
self.assertRedirects(response, '/i18n/', fetch_redirect_response=False)
64+
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
65+
66+
def test_setlang_default_redirect(self):
67+
"""
68+
The set_language view redirects to '/' when there isn't a referer or
69+
"next" parameter.
70+
"""
71+
lang_code = self._get_inactive_language_code()
72+
post_data = dict(language=lang_code)
73+
response = self.client.post('/i18n/setlang/', post_data)
74+
self.assertRedirects(response, '/')
75+
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
76+
77+
def test_setlang_performs_redirect_for_ajax_if_explicitly_requested(self):
78+
"""
79+
The set_language view redirects to the "next" parameter for AJAX calls.
80+
"""
81+
lang_code = self._get_inactive_language_code()
82+
post_data = dict(language=lang_code, next='/')
83+
response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
84+
self.assertRedirects(response, '/')
85+
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
86+
87+
def test_setlang_doesnt_perform_a_redirect_to_referer_for_ajax(self):
88+
"""
89+
The set_language view doesn't redirect to the HTTP referer header for
90+
AJAX calls.
91+
"""
92+
lang_code = self._get_inactive_language_code()
93+
post_data = dict(language=lang_code)
94+
headers = {'HTTP_REFERER': '/', 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
95+
response = self.client.post('/i18n/setlang/', post_data, **headers)
96+
self.assertEqual(response.status_code, 204)
97+
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
98+
99+
def test_setlang_doesnt_perform_a_default_redirect_for_ajax(self):
100+
"""
101+
The set_language view returns 204 for AJAX calls by default.
102+
"""
103+
lang_code = self._get_inactive_language_code()
104+
post_data = dict(language=lang_code)
105+
response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
106+
self.assertEqual(response.status_code, 204)
107+
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
108+
109+
def test_setlang_unsafe_next_for_ajax(self):
110+
"""
111+
The fallback to root URL for the set_language view works for AJAX calls.
112+
"""
113+
lang_code = self._get_inactive_language_code()
114+
post_data = dict(language=lang_code, next='//unsafe/redirection/')
115+
response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
116+
self.assertEqual(response.url, '/')
117+
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
118+
48119
def test_setlang_reversal(self):
49120
self.assertEqual(reverse('set_language'), '/i18n/setlang/')
50121

0 commit comments

Comments
 (0)