Loading django/views/i18n.py +6 −5 Original line number Diff line number Diff line Loading @@ -34,14 +34,15 @@ def set_language(request): any state. """ 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') if not is_safe_url(url=next, host=request.get_host()): next = '/' response = http.HttpResponseRedirect(next) response = http.HttpResponseRedirect(next) if next else http.HttpResponse(status=204) if request.method == 'POST': lang_code = request.POST.get(LANGUAGE_QUERY_PARAMETER) if lang_code and check_for_language(lang_code): if next: next_trans = translate_url(next, lang_code) if next_trans != next: response = http.HttpResponseRedirect(next_trans) Loading docs/releases/1.10.txt +7 −0 Original line number Diff line number Diff line Loading @@ -267,6 +267,10 @@ Internationalization :func:`~django.conf.urls.i18n.i18n_patterns` to ``False``, you can allow 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 ~~~~~~~~~~~~~~~~~~~ Loading Loading @@ -695,6 +699,9 @@ Miscellaneous :meth:`~django.test.Client.login()` method no longer always rejects inactive 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: Features deprecated in 1.10 Loading docs/topics/i18n/translation.txt +15 −8 Original line number Diff line number Diff line Loading @@ -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``. (The name can be changed through the :setting:`LANGUAGE_COOKIE_NAME` setting.) After setting the language choice, Django redirects the user, following this algorithm: * Django looks for a ``next`` parameter in the ``POST`` data. * If that doesn't exist, or is empty, Django tries the URL in the ``Referrer`` header. * 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. After setting the language choice, Django looks for a ``next`` parameter in the ``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: * For AJAX requests, the fallback will be performed only if the ``next`` parameter was set. Otherwise a 204 status code (No Content) will be returned. * For non-AJAX requests, the fallback will always be performed. .. versionchanged:: 1.10 Returning a 204 status code for AJAX requests when no redirect is specified is new. Here's example HTML template code: Loading tests/view_tests/tests/test_i18n.py +78 −7 Original line number Diff line number Diff line Loading @@ -13,7 +13,9 @@ from django.test.selenium import SeleniumTestCase from django.urls import reverse from django.utils import six 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 Loading @@ -22,15 +24,20 @@ from ..urls import locale_dir class I18NTests(TestCase): """ 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): """ The set_language view can be used to change the session language. 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='/') 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.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) Loading @@ -39,12 +46,76 @@ class I18NTests(TestCase): The set_language view only redirects to the 'next' argument if it is "safe". """ lang_code, lang_name = settings.LANGUAGES[0] lang_code = self._get_inactive_language_code() post_data = dict(language=lang_code, next='//unsafe/redirection/') response = self.client.post('/i18n/setlang/', data=post_data) self.assertEqual(response.url, '/') 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): self.assertEqual(reverse('set_language'), '/i18n/setlang/') Loading Loading
django/views/i18n.py +6 −5 Original line number Diff line number Diff line Loading @@ -34,14 +34,15 @@ def set_language(request): any state. """ 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') if not is_safe_url(url=next, host=request.get_host()): next = '/' response = http.HttpResponseRedirect(next) response = http.HttpResponseRedirect(next) if next else http.HttpResponse(status=204) if request.method == 'POST': lang_code = request.POST.get(LANGUAGE_QUERY_PARAMETER) if lang_code and check_for_language(lang_code): if next: next_trans = translate_url(next, lang_code) if next_trans != next: response = http.HttpResponseRedirect(next_trans) Loading
docs/releases/1.10.txt +7 −0 Original line number Diff line number Diff line Loading @@ -267,6 +267,10 @@ Internationalization :func:`~django.conf.urls.i18n.i18n_patterns` to ``False``, you can allow 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 ~~~~~~~~~~~~~~~~~~~ Loading Loading @@ -695,6 +699,9 @@ Miscellaneous :meth:`~django.test.Client.login()` method no longer always rejects inactive 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: Features deprecated in 1.10 Loading
docs/topics/i18n/translation.txt +15 −8 Original line number Diff line number Diff line Loading @@ -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``. (The name can be changed through the :setting:`LANGUAGE_COOKIE_NAME` setting.) After setting the language choice, Django redirects the user, following this algorithm: * Django looks for a ``next`` parameter in the ``POST`` data. * If that doesn't exist, or is empty, Django tries the URL in the ``Referrer`` header. * 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. After setting the language choice, Django looks for a ``next`` parameter in the ``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: * For AJAX requests, the fallback will be performed only if the ``next`` parameter was set. Otherwise a 204 status code (No Content) will be returned. * For non-AJAX requests, the fallback will always be performed. .. versionchanged:: 1.10 Returning a 204 status code for AJAX requests when no redirect is specified is new. Here's example HTML template code: Loading
tests/view_tests/tests/test_i18n.py +78 −7 Original line number Diff line number Diff line Loading @@ -13,7 +13,9 @@ from django.test.selenium import SeleniumTestCase from django.urls import reverse from django.utils import six 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 Loading @@ -22,15 +24,20 @@ from ..urls import locale_dir class I18NTests(TestCase): """ 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): """ The set_language view can be used to change the session language. 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='/') 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.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) Loading @@ -39,12 +46,76 @@ class I18NTests(TestCase): The set_language view only redirects to the 'next' argument if it is "safe". """ lang_code, lang_name = settings.LANGUAGES[0] lang_code = self._get_inactive_language_code() post_data = dict(language=lang_code, next='//unsafe/redirection/') response = self.client.post('/i18n/setlang/', data=post_data) self.assertEqual(response.url, '/') 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): self.assertEqual(reverse('set_language'), '/i18n/setlang/') Loading