mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #15619 -- Deprecated log out via GET requests.
Thanks Florian Apolloner for the implementation idea. Co-Authored-By: Mariusz Felisiak <felisiak.mariusz@gmail.com>
This commit is contained in:
		
				
					committed by
					
						 Mariusz Felisiak
						Mariusz Felisiak
					
				
			
			
				
	
			
			
			
						parent
						
							d4bf3b4c75
						
					
				
				
					commit
					eb07b5be0c
				
			| @@ -1,3 +1,4 @@ | ||||
| import warnings | ||||
| from urllib.parse import urlparse, urlunparse | ||||
|  | ||||
| from django.conf import settings | ||||
| @@ -21,6 +22,7 @@ from django.http import HttpResponseRedirect, QueryDict | ||||
| from django.shortcuts import resolve_url | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.decorators import method_decorator | ||||
| from django.utils.deprecation import RemovedInDjango50Warning | ||||
| from django.utils.http import url_has_allowed_host_and_scheme, urlsafe_base64_decode | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from django.views.decorators.cache import never_cache | ||||
| @@ -117,23 +119,38 @@ class LogoutView(SuccessURLAllowedHostsMixin, TemplateView): | ||||
|     Log out the user and display the 'You are logged out' message. | ||||
|     """ | ||||
|  | ||||
|     # RemovedInDjango50Warning: when the deprecation ends, remove "get" and | ||||
|     # "head" from http_method_names. | ||||
|     http_method_names = ["get", "head", "post", "options"] | ||||
|     next_page = None | ||||
|     redirect_field_name = REDIRECT_FIELD_NAME | ||||
|     template_name = "registration/logged_out.html" | ||||
|     extra_context = None | ||||
|  | ||||
|     # RemovedInDjango50Warning: when the deprecation ends, move | ||||
|     # @method_decorator(csrf_protect) from post() to dispatch(). | ||||
|     @method_decorator(never_cache) | ||||
|     def dispatch(self, request, *args, **kwargs): | ||||
|         if request.method.lower() == "get": | ||||
|             warnings.warn( | ||||
|                 "Log out via GET requests is deprecated and will be removed in Django " | ||||
|                 "5.0. Use POST requests for logging out.", | ||||
|                 RemovedInDjango50Warning, | ||||
|             ) | ||||
|         return super().dispatch(request, *args, **kwargs) | ||||
|  | ||||
|     @method_decorator(csrf_protect) | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         """Logout may be done via POST.""" | ||||
|         auth_logout(request) | ||||
|         next_page = self.get_next_page() | ||||
|         if next_page: | ||||
|             # Redirect to this page until the session has been cleared. | ||||
|             return HttpResponseRedirect(next_page) | ||||
|         return super().dispatch(request, *args, **kwargs) | ||||
|         return super().get(request, *args, **kwargs) | ||||
|  | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         """Logout may be done via POST.""" | ||||
|         return self.get(request, *args, **kwargs) | ||||
|     # RemovedInDjango50Warning. | ||||
|     get = post | ||||
|  | ||||
|     def get_next_page(self): | ||||
|         if self.next_page is not None: | ||||
|   | ||||
| @@ -90,6 +90,10 @@ details on these changes. | ||||
| * ``created=True`` will be required in the signature of | ||||
|   ``RemoteUserBackend.configure_user()`` subclasses. | ||||
|  | ||||
| * Support for logging out via ``GET`` requests in the | ||||
|   ``django.contrib.auth.views.LogoutView`` and | ||||
|   ``django.contrib.auth.views.logout_then_login()`` will be removed. | ||||
|  | ||||
| .. _deprecation-removed-in-4.1: | ||||
|  | ||||
| 4.1 | ||||
|   | ||||
| @@ -446,6 +446,36 @@ Miscellaneous | ||||
| Features deprecated in 4.1 | ||||
| ========================== | ||||
|  | ||||
| Log out via GET | ||||
| --------------- | ||||
|  | ||||
| Logging out via ``GET`` requests to the :py:class:`built-in logout view | ||||
| <django.contrib.auth.views.LogoutView>` is deprecated. Use ``POST`` requests | ||||
| instead. | ||||
|  | ||||
| If you want to retain the user experience of an HTML link, you can use a form | ||||
| that is styled to appear as a link: | ||||
|  | ||||
| .. code-block:: html | ||||
|  | ||||
|   <form id="logout-form" method="post" action="{% url 'admin:logout' %}"> | ||||
|     {% csrf_token %} | ||||
|     <button type="submit">{% translate "Log out" %}</button> | ||||
|   </form> | ||||
|  | ||||
| .. code-block:: css | ||||
|  | ||||
|   #logout-form { | ||||
|     display: inline; | ||||
|   } | ||||
|   #logout-form button { | ||||
|     background: none; | ||||
|     border: none; | ||||
|     cursor: pointer; | ||||
|     padding: 0; | ||||
|     text-decoration: underline; | ||||
|   } | ||||
|  | ||||
| Miscellaneous | ||||
| ------------- | ||||
|  | ||||
|   | ||||
| @@ -1160,7 +1160,12 @@ implementation details see :ref:`using-the-views`. | ||||
|  | ||||
| .. class:: LogoutView | ||||
|  | ||||
|     Logs a user out. | ||||
|     Logs a user out on ``POST`` requests. | ||||
|  | ||||
|     .. deprecated:: 4.1 | ||||
|  | ||||
|         Support for logging out on ``GET`` requests is deprecated and will be | ||||
|         removed in Django 5.0. | ||||
|  | ||||
|     **URL name:** ``logout`` | ||||
|  | ||||
| @@ -1212,7 +1217,7 @@ implementation details see :ref:`using-the-views`. | ||||
|  | ||||
| .. function:: logout_then_login(request, login_url=None) | ||||
|  | ||||
|     Logs a user out, then redirects to the login page. | ||||
|     Logs a user out on ``POST`` requests, then redirects to the login page. | ||||
|  | ||||
|     **URL name:** No default URL provided | ||||
|  | ||||
| @@ -1221,6 +1226,11 @@ implementation details see :ref:`using-the-views`. | ||||
|     * ``login_url``: The URL of the login page to redirect to. | ||||
|       Defaults to :setting:`settings.LOGIN_URL <LOGIN_URL>` if not supplied. | ||||
|  | ||||
|     .. deprecated:: 4.1 | ||||
|  | ||||
|         Support for logging out on ``GET`` requests is deprecated and will be | ||||
|         removed in Django 5.0. | ||||
|  | ||||
| .. class:: PasswordChangeView | ||||
|  | ||||
|     **URL name:** ``password_change`` | ||||
|   | ||||
| @@ -60,13 +60,13 @@ class SignalTestCase(TestCase): | ||||
|     def test_logout_anonymous(self): | ||||
|         # The log_out function will still trigger the signal for anonymous | ||||
|         # users. | ||||
|         self.client.get("/logout/next_page/") | ||||
|         self.client.post("/logout/next_page/") | ||||
|         self.assertEqual(len(self.logged_out), 1) | ||||
|         self.assertIsNone(self.logged_out[0]) | ||||
|  | ||||
|     def test_logout(self): | ||||
|         self.client.login(username="testclient", password="password") | ||||
|         self.client.get("/logout/next_page/") | ||||
|         self.client.post("/logout/next_page/") | ||||
|         self.assertEqual(len(self.logged_out), 1) | ||||
|         self.assertEqual(self.logged_out[0].username, "testclient") | ||||
|  | ||||
|   | ||||
| @@ -29,9 +29,10 @@ from django.core.exceptions import ImproperlyConfigured | ||||
| from django.db import connection | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.middleware.csrf import CsrfViewMiddleware, get_token | ||||
| from django.test import Client, TestCase, override_settings | ||||
| from django.test import Client, TestCase, ignore_warnings, override_settings | ||||
| from django.test.client import RedirectCycleError | ||||
| from django.urls import NoReverseMatch, reverse, reverse_lazy | ||||
| from django.utils.deprecation import RemovedInDjango50Warning | ||||
| from django.utils.http import urlsafe_base64_encode | ||||
|  | ||||
| from .client import PasswordResetConfirmClient | ||||
| @@ -538,7 +539,7 @@ class ChangePasswordTest(AuthViewsTestCase): | ||||
|         ) | ||||
|  | ||||
|     def logout(self): | ||||
|         self.client.get("/logout/") | ||||
|         self.client.post("/logout/") | ||||
|  | ||||
|     def test_password_change_fails_with_invalid_old_password(self): | ||||
|         self.login() | ||||
| @@ -979,7 +980,10 @@ class LogoutThenLoginTests(AuthViewsTestCase): | ||||
|     def test_default_logout_then_login(self): | ||||
|         self.login() | ||||
|         req = HttpRequest() | ||||
|         req.method = "GET" | ||||
|         req.method = "POST" | ||||
|         csrf_token = get_token(req) | ||||
|         req.COOKIES[settings.CSRF_COOKIE_NAME] = csrf_token | ||||
|         req.POST = {"csrfmiddlewaretoken": csrf_token} | ||||
|         req.session = self.client.session | ||||
|         response = logout_then_login(req) | ||||
|         self.confirm_logged_out() | ||||
| @@ -988,12 +992,28 @@ class LogoutThenLoginTests(AuthViewsTestCase): | ||||
|     def test_logout_then_login_with_custom_login(self): | ||||
|         self.login() | ||||
|         req = HttpRequest() | ||||
|         req.method = "GET" | ||||
|         req.method = "POST" | ||||
|         csrf_token = get_token(req) | ||||
|         req.COOKIES[settings.CSRF_COOKIE_NAME] = csrf_token | ||||
|         req.POST = {"csrfmiddlewaretoken": csrf_token} | ||||
|         req.session = self.client.session | ||||
|         response = logout_then_login(req, login_url="/custom/") | ||||
|         self.confirm_logged_out() | ||||
|         self.assertRedirects(response, "/custom/", fetch_redirect_response=False) | ||||
|  | ||||
|     @ignore_warnings(category=RemovedInDjango50Warning) | ||||
|     @override_settings(LOGIN_URL="/login/") | ||||
|     def test_default_logout_then_login_get(self): | ||||
|         self.login() | ||||
|         req = HttpRequest() | ||||
|         req.method = "GET" | ||||
|         req.session = self.client.session | ||||
|         response = logout_then_login(req) | ||||
|         # RemovedInDjango50Warning: When the deprecation ends, replace with | ||||
|         #   self.assertEqual(response.status_code, 405) | ||||
|         self.confirm_logged_out() | ||||
|         self.assertRedirects(response, "/login/", fetch_redirect_response=False) | ||||
|  | ||||
|  | ||||
| class LoginRedirectAuthenticatedUser(AuthViewsTestCase): | ||||
|     dont_redirect_url = "/login/redirect_authenticated_user_default/" | ||||
| @@ -1136,7 +1156,7 @@ class LogoutTest(AuthViewsTestCase): | ||||
|     def test_logout_default(self): | ||||
|         "Logout without next_page option renders the default template" | ||||
|         self.login() | ||||
|         response = self.client.get("/logout/") | ||||
|         response = self.client.post("/logout/") | ||||
|         self.assertContains(response, "Logged out") | ||||
|         self.confirm_logged_out() | ||||
|  | ||||
| @@ -1146,10 +1166,21 @@ class LogoutTest(AuthViewsTestCase): | ||||
|         self.assertContains(response, "Logged out") | ||||
|         self.confirm_logged_out() | ||||
|  | ||||
|     def test_logout_with_get_raises_deprecation_warning(self): | ||||
|         self.login() | ||||
|         msg = ( | ||||
|             "Log out via GET requests is deprecated and will be removed in Django 5.0. " | ||||
|             "Use POST requests for logging out." | ||||
|         ) | ||||
|         with self.assertWarnsMessage(RemovedInDjango50Warning, msg): | ||||
|             response = self.client.get("/logout/") | ||||
|         self.assertContains(response, "Logged out") | ||||
|         self.confirm_logged_out() | ||||
|  | ||||
|     def test_14377(self): | ||||
|         # Bug 14377 | ||||
|         self.login() | ||||
|         response = self.client.get("/logout/") | ||||
|         response = self.client.post("/logout/") | ||||
|         self.assertIn("site", response.context) | ||||
|  | ||||
|     def test_logout_doesnt_cache(self): | ||||
| @@ -1157,16 +1188,16 @@ class LogoutTest(AuthViewsTestCase): | ||||
|         The logout() view should send "no-cache" headers for reasons described | ||||
|         in #25490. | ||||
|         """ | ||||
|         response = self.client.get("/logout/") | ||||
|         response = self.client.post("/logout/") | ||||
|         self.assertIn("no-store", response.headers["Cache-Control"]) | ||||
|  | ||||
|     def test_logout_with_overridden_redirect_url(self): | ||||
|         # Bug 11223 | ||||
|         self.login() | ||||
|         response = self.client.get("/logout/next_page/") | ||||
|         response = self.client.post("/logout/next_page/") | ||||
|         self.assertRedirects(response, "/somewhere/", fetch_redirect_response=False) | ||||
|  | ||||
|         response = self.client.get("/logout/next_page/?next=/login/") | ||||
|         response = self.client.post("/logout/next_page/?next=/login/") | ||||
|         self.assertRedirects(response, "/login/", fetch_redirect_response=False) | ||||
|  | ||||
|         self.confirm_logged_out() | ||||
| @@ -1174,28 +1205,28 @@ class LogoutTest(AuthViewsTestCase): | ||||
|     def test_logout_with_next_page_specified(self): | ||||
|         "Logout with next_page option given redirects to specified resource" | ||||
|         self.login() | ||||
|         response = self.client.get("/logout/next_page/") | ||||
|         response = self.client.post("/logout/next_page/") | ||||
|         self.assertRedirects(response, "/somewhere/", fetch_redirect_response=False) | ||||
|         self.confirm_logged_out() | ||||
|  | ||||
|     def test_logout_with_redirect_argument(self): | ||||
|         "Logout with query string redirects to specified resource" | ||||
|         self.login() | ||||
|         response = self.client.get("/logout/?next=/login/") | ||||
|         response = self.client.post("/logout/?next=/login/") | ||||
|         self.assertRedirects(response, "/login/", fetch_redirect_response=False) | ||||
|         self.confirm_logged_out() | ||||
|  | ||||
|     def test_logout_with_custom_redirect_argument(self): | ||||
|         "Logout with custom query string redirects to specified resource" | ||||
|         self.login() | ||||
|         response = self.client.get("/logout/custom_query/?follow=/somewhere/") | ||||
|         response = self.client.post("/logout/custom_query/?follow=/somewhere/") | ||||
|         self.assertRedirects(response, "/somewhere/", fetch_redirect_response=False) | ||||
|         self.confirm_logged_out() | ||||
|  | ||||
|     def test_logout_with_named_redirect(self): | ||||
|         "Logout resolves names or URLs passed as next_page." | ||||
|         self.login() | ||||
|         response = self.client.get("/logout/next_page/named/") | ||||
|         response = self.client.post("/logout/next_page/named/") | ||||
|         self.assertRedirects( | ||||
|             response, "/password_reset/", fetch_redirect_response=False | ||||
|         ) | ||||
| @@ -1203,7 +1234,7 @@ class LogoutTest(AuthViewsTestCase): | ||||
|  | ||||
|     def test_success_url_allowed_hosts_same_host(self): | ||||
|         self.login() | ||||
|         response = self.client.get("/logout/allowed_hosts/?next=https://testserver/") | ||||
|         response = self.client.post("/logout/allowed_hosts/?next=https://testserver/") | ||||
|         self.assertRedirects( | ||||
|             response, "https://testserver/", fetch_redirect_response=False | ||||
|         ) | ||||
| @@ -1211,7 +1242,7 @@ class LogoutTest(AuthViewsTestCase): | ||||
|  | ||||
|     def test_success_url_allowed_hosts_safe_host(self): | ||||
|         self.login() | ||||
|         response = self.client.get("/logout/allowed_hosts/?next=https://otherserver/") | ||||
|         response = self.client.post("/logout/allowed_hosts/?next=https://otherserver/") | ||||
|         self.assertRedirects( | ||||
|             response, "https://otherserver/", fetch_redirect_response=False | ||||
|         ) | ||||
| @@ -1219,7 +1250,7 @@ class LogoutTest(AuthViewsTestCase): | ||||
|  | ||||
|     def test_success_url_allowed_hosts_unsafe_host(self): | ||||
|         self.login() | ||||
|         response = self.client.get("/logout/allowed_hosts/?next=https://evil/") | ||||
|         response = self.client.post("/logout/allowed_hosts/?next=https://evil/") | ||||
|         self.assertRedirects( | ||||
|             response, "/logout/allowed_hosts/", fetch_redirect_response=False | ||||
|         ) | ||||
| @@ -1246,7 +1277,7 @@ class LogoutTest(AuthViewsTestCase): | ||||
|                     "bad_url": quote(bad_url), | ||||
|                 } | ||||
|                 self.login() | ||||
|                 response = self.client.get(nasty_url) | ||||
|                 response = self.client.post(nasty_url) | ||||
|                 self.assertEqual(response.status_code, 302) | ||||
|                 self.assertNotIn( | ||||
|                     bad_url, response.url, "%s should be blocked" % bad_url | ||||
| @@ -1272,7 +1303,7 @@ class LogoutTest(AuthViewsTestCase): | ||||
|                     "good_url": quote(good_url), | ||||
|                 } | ||||
|                 self.login() | ||||
|                 response = self.client.get(safe_url) | ||||
|                 response = self.client.post(safe_url) | ||||
|                 self.assertEqual(response.status_code, 302) | ||||
|                 self.assertIn(good_url, response.url, "%s should be allowed" % good_url) | ||||
|                 self.confirm_logged_out() | ||||
| @@ -1286,7 +1317,7 @@ class LogoutTest(AuthViewsTestCase): | ||||
|             "next_url": quote(non_https_next_url), | ||||
|         } | ||||
|         self.login() | ||||
|         response = self.client.get(url, secure=True) | ||||
|         response = self.client.post(url, secure=True) | ||||
|         self.assertRedirects(response, logout_url, fetch_redirect_response=False) | ||||
|         self.confirm_logged_out() | ||||
|  | ||||
| @@ -1295,19 +1326,19 @@ class LogoutTest(AuthViewsTestCase): | ||||
|         self.login() | ||||
|         self.client.post("/setlang/", {"language": "pl"}) | ||||
|         self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, "pl") | ||||
|         self.client.get("/logout/") | ||||
|         self.client.post("/logout/") | ||||
|         self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, "pl") | ||||
|  | ||||
|     @override_settings(LOGOUT_REDIRECT_URL="/custom/") | ||||
|     def test_logout_redirect_url_setting(self): | ||||
|         self.login() | ||||
|         response = self.client.get("/logout/") | ||||
|         response = self.client.post("/logout/") | ||||
|         self.assertRedirects(response, "/custom/", fetch_redirect_response=False) | ||||
|  | ||||
|     @override_settings(LOGOUT_REDIRECT_URL="logout") | ||||
|     def test_logout_redirect_url_named_setting(self): | ||||
|         self.login() | ||||
|         response = self.client.get("/logout/") | ||||
|         response = self.client.post("/logout/") | ||||
|         self.assertRedirects(response, "/logout/", fetch_redirect_response=False) | ||||
|  | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user