Fixed #21446 -- Allowed not performing redirect in set_language view

Thanks Claude Paroz and Tim Graham for polishing the patch.
This commit is contained in:
Krzysztof Jurewicz 2016-03-28 19:23:04 +02:00 committed by Claude Paroz
parent 12ba20d83c
commit 940b7fd5cb
4 changed files with 105 additions and 19 deletions

View File

@ -34,17 +34,18 @@ def set_language(request):
any state. any state.
""" """
next = request.POST.get('next', request.GET.get('next')) next = request.POST.get('next', request.GET.get('next'))
if not is_safe_url(url=next, host=request.get_host()): if (next or not request.is_ajax()) and not is_safe_url(url=next, host=request.get_host()):
next = request.META.get('HTTP_REFERER') next = request.META.get('HTTP_REFERER')
if not is_safe_url(url=next, host=request.get_host()): if not is_safe_url(url=next, host=request.get_host()):
next = '/' next = '/'
response = http.HttpResponseRedirect(next) response = http.HttpResponseRedirect(next) if next else http.HttpResponse(status=204)
if request.method == 'POST': if request.method == 'POST':
lang_code = request.POST.get(LANGUAGE_QUERY_PARAMETER) lang_code = request.POST.get(LANGUAGE_QUERY_PARAMETER)
if lang_code and check_for_language(lang_code): if lang_code and check_for_language(lang_code):
next_trans = translate_url(next, lang_code) if next:
if next_trans != next: next_trans = translate_url(next, lang_code)
response = http.HttpResponseRedirect(next_trans) if next_trans != next:
response = http.HttpResponseRedirect(next_trans)
if hasattr(request, 'session'): if hasattr(request, 'session'):
request.session[LANGUAGE_SESSION_KEY] = lang_code request.session[LANGUAGE_SESSION_KEY] = lang_code
else: else:

View File

@ -267,6 +267,10 @@ Internationalization
:func:`~django.conf.urls.i18n.i18n_patterns` to ``False``, you can allow :func:`~django.conf.urls.i18n.i18n_patterns` to ``False``, you can allow
accessing the default language without a URL prefix. accessing the default language without a URL prefix.
* :func:`~django.views.i18n.set_language` now returns a 204 status code (No
Content) for AJAX requests when there is no ``next`` parameter in ``POST`` or
``GET``.
Management Commands Management Commands
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
@ -695,6 +699,9 @@ Miscellaneous
:meth:`~django.test.Client.login()` method no longer always rejects inactive :meth:`~django.test.Client.login()` method no longer always rejects inactive
users but instead delegates this decision to the authentication backend. users but instead delegates this decision to the authentication backend.
* :func:`django.views.i18n.set_language` may now return a 204 status code for
AJAX requests.
.. _deprecated-features-1.10: .. _deprecated-features-1.10:
Features deprecated in 1.10 Features deprecated in 1.10

View File

@ -1788,14 +1788,21 @@ saves the language choice in the user's session. Otherwise, it saves the
language choice in a cookie that is by default named ``django_language``. language choice in a cookie that is by default named ``django_language``.
(The name can be changed through the :setting:`LANGUAGE_COOKIE_NAME` setting.) (The name can be changed through the :setting:`LANGUAGE_COOKIE_NAME` setting.)
After setting the language choice, Django redirects the user, following this After setting the language choice, Django looks for a ``next`` parameter in the
algorithm: ``POST`` or ``GET`` data. If that is found and Django considers it to be a safe
URL (i.e. it doesn't point to a different host and uses a safe scheme), a
redirect to that URL will be performed. Otherwise, Django may fall back to
redirecting the user to the URL from the ``Referer`` header or, if it is not
set, to ``/``, depending on the nature of the request:
* Django looks for a ``next`` parameter in the ``POST`` data. * For AJAX requests, the fallback will be performed only if the ``next``
* If that doesn't exist, or is empty, Django tries the URL in the parameter was set. Otherwise a 204 status code (No Content) will be returned.
``Referrer`` header. * For non-AJAX requests, the fallback will always be performed.
* If that's empty -- say, if a user's browser suppresses that header --
then the user will be redirected to ``/`` (the site root) as a fallback. .. versionchanged:: 1.10
Returning a 204 status code for AJAX requests when no redirect is specified
is new.
Here's example HTML template code: Here's example HTML template code:

View File

@ -13,7 +13,9 @@ from django.test.selenium import SeleniumTestCase
from django.urls import reverse from django.urls import reverse
from django.utils import six from django.utils import six
from django.utils._os import upath from django.utils._os import upath
from django.utils.translation import LANGUAGE_SESSION_KEY, override from django.utils.translation import (
LANGUAGE_SESSION_KEY, get_language, override,
)
from ..urls import locale_dir from ..urls import locale_dir
@ -22,29 +24,98 @@ from ..urls import locale_dir
class I18NTests(TestCase): class I18NTests(TestCase):
""" Tests django views in django/views/i18n.py """ """ Tests django views in django/views/i18n.py """
def _get_inactive_language_code(self):
"""Return language code for a language which is not activated."""
current_language = get_language()
return [code for code, name in settings.LANGUAGES if not code == current_language][0]
def test_setlang(self): def test_setlang(self):
""" """
The set_language view can be used to change the session language. The set_language view can be used to change the session language.
The user is redirected to the 'next' argument if provided. The user is redirected to the 'next' argument if provided.
""" """
for lang_code, lang_name in settings.LANGUAGES: lang_code = self._get_inactive_language_code()
post_data = dict(language=lang_code, next='/') post_data = dict(language=lang_code, next='/')
response = self.client.post('/i18n/setlang/', data=post_data) response = self.client.post('/i18n/setlang/', post_data, HTTP_REFERER='/i_should_not_be_used/')
self.assertRedirects(response, '/') self.assertRedirects(response, '/')
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_unsafe_next(self): def test_setlang_unsafe_next(self):
""" """
The set_language view only redirects to the 'next' argument if it is The set_language view only redirects to the 'next' argument if it is
"safe". "safe".
""" """
lang_code, lang_name = settings.LANGUAGES[0] lang_code = self._get_inactive_language_code()
post_data = dict(language=lang_code, next='//unsafe/redirection/') post_data = dict(language=lang_code, next='//unsafe/redirection/')
response = self.client.post('/i18n/setlang/', data=post_data) response = self.client.post('/i18n/setlang/', data=post_data)
self.assertEqual(response.url, '/') self.assertEqual(response.url, '/')
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_redirect_to_referer(self):
"""
The set_language view redirects to the URL in the referer header when
there isn't a "next" parameter.
"""
lang_code = self._get_inactive_language_code()
post_data = dict(language=lang_code)
response = self.client.post('/i18n/setlang/', post_data, HTTP_REFERER='/i18n/')
self.assertRedirects(response, '/i18n/', fetch_redirect_response=False)
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_default_redirect(self):
"""
The set_language view redirects to '/' when there isn't a referer or
"next" parameter.
"""
lang_code = self._get_inactive_language_code()
post_data = dict(language=lang_code)
response = self.client.post('/i18n/setlang/', post_data)
self.assertRedirects(response, '/')
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_performs_redirect_for_ajax_if_explicitly_requested(self):
"""
The set_language view redirects to the "next" parameter for AJAX calls.
"""
lang_code = self._get_inactive_language_code()
post_data = dict(language=lang_code, next='/')
response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertRedirects(response, '/')
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_doesnt_perform_a_redirect_to_referer_for_ajax(self):
"""
The set_language view doesn't redirect to the HTTP referer header for
AJAX calls.
"""
lang_code = self._get_inactive_language_code()
post_data = dict(language=lang_code)
headers = {'HTTP_REFERER': '/', 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
response = self.client.post('/i18n/setlang/', post_data, **headers)
self.assertEqual(response.status_code, 204)
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_doesnt_perform_a_default_redirect_for_ajax(self):
"""
The set_language view returns 204 for AJAX calls by default.
"""
lang_code = self._get_inactive_language_code()
post_data = dict(language=lang_code)
response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 204)
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_unsafe_next_for_ajax(self):
"""
The fallback to root URL for the set_language view works for AJAX calls.
"""
lang_code = self._get_inactive_language_code()
post_data = dict(language=lang_code, next='//unsafe/redirection/')
response = self.client.post('/i18n/setlang/', post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.url, '/')
self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code)
def test_setlang_reversal(self): def test_setlang_reversal(self):
self.assertEqual(reverse('set_language'), '/i18n/setlang/') self.assertEqual(reverse('set_language'), '/i18n/setlang/')