diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index dd368584e5..61d08ddba5 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -154,6 +154,9 @@ LANGUAGE_COOKIE_NAME = 'django_language' LANGUAGE_COOKIE_AGE = None LANGUAGE_COOKIE_DOMAIN = None LANGUAGE_COOKIE_PATH = '/' +LANGUAGE_COOKIE_SECURE = False +LANGUAGE_COOKIE_HTTPONLY = False +LANGUAGE_COOKIE_SAMESITE = None # If you set this to True, Django will format dates, numbers and calendars diff --git a/django/views/i18n.py b/django/views/i18n.py index ce5691632c..e3ef40b2fc 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -55,6 +55,9 @@ def set_language(request): 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, ) return response diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 5c87aa5d24..ae7436696e 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1766,6 +1766,21 @@ permanently (via the :setting:`LANGUAGE_COOKIE_NAME` setting) and to add a middleware that copies the value from the old cookie to a new one and then deletes the old one. +.. setting:: LANGUAGE_COOKIE_HTTPONLY + +``LANGUAGE_COOKIE_HTTPONLY`` +---------------------------- + +.. versionadded:: 3.0 + +Default: ``False`` + +Whether to use ``HttpOnly`` flag on the language cookie. If this is set to +``True``, client-side JavaScript will not to be able to access the language +cookie. + +See :setting:`SESSION_COOKIE_HTTPONLY` for details on ``HttpOnly``. + .. setting:: LANGUAGE_COOKIE_NAME ``LANGUAGE_COOKIE_NAME`` @@ -1800,6 +1815,33 @@ permanently (via the :setting:`LANGUAGE_COOKIE_NAME` setting), and to add a middleware that copies the value from the old cookie to a new one and then deletes the one. +.. setting:: LANGUAGE_COOKIE_SAMESITE + +``LANGUAGE_COOKIE_SAMESITE`` +---------------------------- + +.. versionadded:: 3.0 + +Default: ``None`` + +The value of the `SameSite`_ flag on the language cookie. This flag prevents the +cookie from being sent in cross-site requests. + +See :setting:`SESSION_COOKIE_SAMESITE` for details about ``SameSite``. + +.. setting:: LANGUAGE_COOKIE_SECURE + +``LANGUAGE_COOKIE_SECURE`` +-------------------------- + +.. versionadded:: 3.0 + +Default: ``False`` + +Whether to use a secure cookie for the language cookie. If this is set to +``True``, the cookie will be marked as "secure", which means browsers may +ensure that the cookie is only sent under an HTTPS connection. + .. setting:: LANGUAGES ``LANGUAGES`` @@ -3402,8 +3444,11 @@ Globalization (``i18n``/``l10n``) * :setting:`LANGUAGE_CODE` * :setting:`LANGUAGE_COOKIE_AGE` * :setting:`LANGUAGE_COOKIE_DOMAIN` +* :setting:`LANGUAGE_COOKIE_HTTPONLY` * :setting:`LANGUAGE_COOKIE_NAME` * :setting:`LANGUAGE_COOKIE_PATH` +* :setting:`LANGUAGE_COOKIE_SAMESITE` +* :setting:`LANGUAGE_COOKIE_SECURE` * :setting:`LANGUAGES` * :setting:`LANGUAGES_BIDI` * :setting:`LOCALE_PATHS` diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index 11257b70be..d7ad7a8cf7 100644 --- a/docs/releases/3.0.txt +++ b/docs/releases/3.0.txt @@ -153,7 +153,11 @@ Generic Views Internationalization ~~~~~~~~~~~~~~~~~~~~ -* ... +* Added the :setting:`LANGUAGE_COOKIE_HTTPONLY`, + :setting:`LANGUAGE_COOKIE_SAMESITE`, and :setting:`LANGUAGE_COOKIE_SECURE` + settings to set the ``HttpOnly``, ``SameSite``, and ``Secure`` flags on + language cookies. The default values of these settings preserve the previous + behavior. Management Commands ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 00338100f9..bafa4bdaf1 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -1896,7 +1896,10 @@ A number of settings can be used to adjust language cookie options: * :setting:`LANGUAGE_COOKIE_NAME` * :setting:`LANGUAGE_COOKIE_AGE` * :setting:`LANGUAGE_COOKIE_DOMAIN` +* :setting:`LANGUAGE_COOKIE_HTTPONLY` * :setting:`LANGUAGE_COOKIE_PATH` +* :setting:`LANGUAGE_COOKIE_SAMESITE` +* :setting:`LANGUAGE_COOKIE_SECURE` Implementation notes ==================== diff --git a/tests/view_tests/tests/test_i18n.py b/tests/view_tests/tests/test_i18n.py index 7b36181800..1362bc2911 100644 --- a/tests/view_tests/tests/test_i18n.py +++ b/tests/view_tests/tests/test_i18n.py @@ -45,6 +45,9 @@ class SetLanguageTests(TestCase): self.assertEqual(language_cookie['domain'], '') self.assertEqual(language_cookie['path'], '/') self.assertEqual(language_cookie['max-age'], '') + self.assertEqual(language_cookie['httponly'], '') + self.assertEqual(language_cookie['samesite'], '') + self.assertEqual(language_cookie['secure'], '') def test_setlang_unsafe_next(self): """ @@ -175,6 +178,9 @@ class SetLanguageTests(TestCase): 'LANGUAGE_COOKIE_AGE': 3600 * 7 * 2, 'LANGUAGE_COOKIE_DOMAIN': '.example.com', 'LANGUAGE_COOKIE_PATH': '/test/', + 'LANGUAGE_COOKIE_HTTPONLY': True, + 'LANGUAGE_COOKIE_SAMESITE': 'Strict', + 'LANGUAGE_COOKIE_SECURE': True, } with self.settings(**test_settings): post_data = {'language': 'pl', 'next': '/views/'} @@ -184,6 +190,9 @@ class SetLanguageTests(TestCase): self.assertEqual(language_cookie['domain'], '.example.com') self.assertEqual(language_cookie['path'], '/test/') self.assertEqual(language_cookie['max-age'], 3600 * 7 * 2) + self.assertEqual(language_cookie['httponly'], True) + self.assertEqual(language_cookie['samesite'], 'Strict') + self.assertEqual(language_cookie['secure'], True) def test_setlang_decodes_http_referer_url(self): """