diff --git a/ChangeLog.rst b/ChangeLog.rst index e084f3ab8e..36d2e8826d 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -8,6 +8,10 @@ Note worthy changes - Added a new provider, Atlassian +- Next URL handling been streamlined to be consistently applied. Previously, the + password reset, change and email confirmation views only supported the + ``success_url`` class-level property. + Backwards incompatible changes ------------------------------ diff --git a/allauth/account/mixins.py b/allauth/account/mixins.py new file mode 100644 index 0000000000..34c17a8924 --- /dev/null +++ b/allauth/account/mixins.py @@ -0,0 +1,178 @@ +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.core.exceptions import ImproperlyConfigured +from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect +from django.utils.html import format_html + +from allauth.account import app_settings +from allauth.account.adapter import get_adapter +from allauth.account.internal import flows +from allauth.account.utils import ( + get_login_redirect_url, + get_next_redirect_url, + passthrough_next_redirect_url, +) +from allauth.core.exceptions import ImmediateHttpResponse +from allauth.utils import get_request_param + + +def _ajax_response(request, response, form=None, data=None): + adapter = get_adapter() + if adapter.is_ajax(request): + if isinstance(response, HttpResponseRedirect) or isinstance( + response, HttpResponsePermanentRedirect + ): + redirect_to = response["Location"] + else: + redirect_to = None + response = adapter.ajax_response( + request, response, form=form, data=data, redirect_to=redirect_to + ) + return response + + +class RedirectAuthenticatedUserMixin: + def dispatch(self, request, *args, **kwargs): + if request.user.is_authenticated and app_settings.AUTHENTICATED_LOGIN_REDIRECTS: + redirect_to = self.get_authenticated_redirect_url() + response = HttpResponseRedirect(redirect_to) + return _ajax_response(request, response) + else: + response = super().dispatch(request, *args, **kwargs) + return response + + def get_authenticated_redirect_url(self): + redirect_field_name = self.redirect_field_name + return get_login_redirect_url( + self.request, + url=self.get_success_url(), + redirect_field_name=redirect_field_name, + ) + + +class LogoutFunctionalityMixin: + def logout(self): + flows.logout.logout(self.request) + + +class AjaxCapableProcessFormViewMixin: + def get(self, request, *args, **kwargs): + response = super().get(request, *args, **kwargs) + form = self.get_form() + return _ajax_response( + self.request, response, form=form, data=self._get_ajax_data_if() + ) + + def post(self, request, *args, **kwargs): + form_class = self.get_form_class() + form = self.get_form(form_class) + if form.is_valid(): + response = self.form_valid(form) + else: + response = self.form_invalid(form) + return _ajax_response( + self.request, response, form=form, data=self._get_ajax_data_if() + ) + + def get_form(self, form_class=None): + form = getattr(self, "_cached_form", None) + if form is None: + form = super().get_form(form_class) + self._cached_form = form + return form + + def _get_ajax_data_if(self): + return ( + self.get_ajax_data() + if get_adapter(self.request).is_ajax(self.request) + else None + ) + + def get_ajax_data(self): + return None + + +class CloseableSignupMixin: + template_name_signup_closed = ( + "account/signup_closed." + app_settings.TEMPLATE_EXTENSION + ) + + def dispatch(self, request, *args, **kwargs): + try: + if not self.is_open(): + return self.closed() + except ImmediateHttpResponse as e: + return e.response + return super().dispatch(request, *args, **kwargs) + + def is_open(self): + return get_adapter(self.request).is_open_for_signup(self.request) + + def closed(self): + response_kwargs = { + "request": self.request, + "template": self.template_name_signup_closed, + } + return self.response_class(**response_kwargs) + + +class NextRedirectMixin: + redirect_field_name = REDIRECT_FIELD_NAME + + def get_context_data(self, **kwargs): + ret = super().get_context_data(**kwargs) + redirect_field_value = get_request_param(self.request, self.redirect_field_name) + ret.update( + { + "redirect_field_name": self.redirect_field_name, + "redirect_field_value": redirect_field_value, + "redirect_field": format_html( + '', + self.redirect_field_name, + redirect_field_value, + ) + if redirect_field_value + else "", + } + ) + return ret + + def get_success_url(self): + """ + We're in a mixin, so we cannot rely on the fact that our super() has a get_success_url. + Also, we want to check for -- in this order: + 1) The `?next=/foo` + 2) The `get_succes_url()` if available. + 3) The `.success_url` if available. + 4) A fallback default success URL: `get_default_success_url()`. + """ + url = self.get_next_url() + if url: + return url + + if not url: + if hasattr(super(), "get_success_url"): + try: + url = super().get_success_url() + except ImproperlyConfigured: + # Django's default get_success_url() checks self.succes_url, + # and throws this if that is not set. Yet, in our case, we + # want to fallback to the default. + pass + elif hasattr(self, "success_url"): + url = self.success_url + if url: + url = str(url) # reverse_lazy + if not url: + url = self.get_default_success_url() + return url + + def get_default_success_url(self): + return None + + def get_next_url(self): + return get_next_redirect_url(self.request, self.redirect_field_name) + + def passthrough_next_url(self, url): + return passthrough_next_redirect_url( + self.request, url, self.redirect_field_name + ) diff --git a/allauth/account/tests/test_change_password.py b/allauth/account/tests/test_change_password.py index acc95f5325..81bbc75ca7 100644 --- a/allauth/account/tests/test_change_password.py +++ b/allauth/account/tests/test_change_password.py @@ -19,16 +19,13 @@ def test_set_usable_password_redirects_to_change(auth_client, user): @pytest.mark.parametrize( - "logout,redirect_chain", + "logout,next_url,redirect_chain", [ - ( - False, - [ - (reverse("account_change_password"), 302), - ], - ), + (False, "", [(reverse("account_change_password"), 302)]), + (False, "/foo", [("/foo", 302)]), ( True, + "", [ (reverse("account_change_password"), 302), ( @@ -37,33 +34,36 @@ def test_set_usable_password_redirects_to_change(auth_client, user): ), ], ), + (True, "/foo", [("/foo", 302)]), ], ) -def test_set_password(client, user, password_factory, logout, settings, redirect_chain): +def test_set_password( + client, user, next_url, password_factory, logout, settings, redirect_chain +): settings.ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = logout user.set_unusable_password() user.save() client.force_login(user) password = password_factory() + data = {"password1": password, "password2": password} + if next_url: + data["next"] = next_url resp = client.post( reverse("account_set_password"), - {"password1": password, "password2": password}, + data, follow=True, ) assert resp.redirect_chain == redirect_chain @pytest.mark.parametrize( - "logout,redirect_chain", + "logout,next_url,redirect_chain", [ - ( - False, - [ - (reverse("account_change_password"), 302), - ], - ), + (False, "", [(reverse("account_change_password"), 302)]), + (False, "/foo", [("/foo", 302)]), ( True, + "", [ (reverse("account_change_password"), 302), ( @@ -72,12 +72,14 @@ def test_set_password(client, user, password_factory, logout, settings, redirect ), ], ), + (True, "/foo", [("/foo", 302)]), ], ) def test_change_password( auth_client, user, user_password, + next_url, password_factory, logout, settings, @@ -87,9 +89,12 @@ def test_change_password( settings.ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = logout settings.ACCOUNT_EMAIL_NOTIFICATIONS = True password = password_factory() + data = {"oldpassword": user_password, "password1": password, "password2": password} + if next_url: + data["next"] = next_url resp = auth_client.post( reverse("account_change_password"), - {"oldpassword": user_password, "password1": password, "password2": password}, + data, follow=True, ) assert resp.redirect_chain == redirect_chain diff --git a/allauth/account/tests/test_confirm_email.py b/allauth/account/tests/test_confirm_email.py index 5661254128..25fb1c3dd4 100644 --- a/allauth/account/tests/test_confirm_email.py +++ b/allauth/account/tests/test_confirm_email.py @@ -8,6 +8,7 @@ from django.urls import reverse from django.utils.timezone import now +import pytest from pytest_django.asserts import ( assertRedirects, assertTemplateNotUsed, @@ -26,7 +27,14 @@ from .test_models import UUIDUser -def test_login_on_confirm(user_factory, client): +@pytest.mark.parametrize( + "query,expected_location", + [ + ("", settings.LOGIN_REDIRECT_URL), + ("?next=/foo", "/foo"), + ], +) +def test_login_on_confirm(user_factory, client, query, expected_location): settings.ACCOUNT_EMAIL_CONFIRMATION_HMAC = True settings.ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True user = user_factory(email_verified=False) @@ -41,7 +49,8 @@ def test_login_on_confirm(user_factory, client): session["account_user"] = user_pk_to_url_str(user) session.save() - resp = client.post(reverse("account_confirm_email", args=[key])) + resp = client.post(reverse("account_confirm_email", args=[key]) + query) + assert resp["location"] == expected_location email = EmailAddress.objects.get(pk=email.pk) assert email.verified diff --git a/allauth/account/tests/test_reset_password.py b/allauth/account/tests/test_reset_password.py index e9d627245e..e94f7faae6 100644 --- a/allauth/account/tests/test_reset_password.py +++ b/allauth/account/tests/test_reset_password.py @@ -5,15 +5,30 @@ from django.core import mail from django.test.utils import override_settings from django.urls import reverse +from django.utils.http import urlencode import pytest +from pytest_django.asserts import assertRedirects, assertTemplateUsed from allauth.account import app_settings -from allauth.account.forms import ResetPasswordForm +from allauth.account.forms import ResetPasswordForm, default_token_generator from allauth.account.models import EmailAddress +from allauth.account.utils import user_pk_to_url_str from allauth.tests import TestCase +@pytest.fixture +def password_reset_url(): + def f(user): + temp_key = default_token_generator.make_token(user) + uid = user_pk_to_url_str(user) + return reverse( + "account_reset_password_from_key", kwargs={"uidb36": uid, "key": temp_key} + ) + + return f + + @pytest.mark.django_db def test_reset_password_unknown_account(client, settings): settings.ACCOUNT_PREVENT_ENUMERATION = True @@ -36,6 +51,18 @@ def test_reset_password_unknown_account_disabled(client, settings): assert len(mail.outbox) == 0 +@pytest.mark.parametrize( + "query,expected_location", + [("", reverse("account_reset_password_done")), ("?next=/foo", "/foo")], +) +def test_reset_password_next_url(client, user, query, expected_location): + resp = client.post( + reverse("account_reset_password") + query, + data={"email": user.email}, + ) + assert resp["location"] == expected_location + + @override_settings( ACCOUNT_PREVENT_ENUMERATION=False, ACCOUNT_DEFAULT_HTTP_PROTOCOL="https", @@ -133,72 +160,6 @@ def test_password_reset_flow_with_empty_session(self): self.assertTrue(resp.context_data["token_fail"]) - def test_password_reset_flow(self): - """ - Tests the password reset flow: requesting a new password, - receiving the reset link via email and finally resetting the - password to a new value. - """ - # Request new password - user = self._request_new_password() - body = mail.outbox[0].body - self.assertGreater(body.find("https://"), 0) - - # Extract URL for `password_reset_from_key` view and access it - url = body[body.find("/password/reset/") :].split()[0] - resp = self.client.get(url) - # Follow the redirect the actual password reset page with the key - # hidden. - url = resp.url - resp = self.client.get(url) - self.assertTemplateUsed( - resp, - "account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION, - ) - self.assertFalse("token_fail" in resp.context_data) - - # Reset the password - resp = self.client.post( - url, {"password1": "newpass123", "password2": "newpass123"} - ) - self.assertRedirects(resp, reverse("account_reset_password_from_key_done")) - assert "Your password has been reset" in mail.outbox[-1].body - - # Check the new password is in effect - user = get_user_model().objects.get(pk=user.pk) - self.assertTrue(user.check_password("newpass123")) - - # Trying to reset the password against the same URL (or any other - # invalid/obsolete URL) returns a bad token response - resp = self.client.post( - url, {"password1": "newpass123", "password2": "newpass123"} - ) - self.assertTemplateUsed( - resp, - "account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION, - ) - self.assertTrue(resp.context_data["token_fail"]) - - # Same should happen when accessing the page directly - response = self.client.get(url) - self.assertTemplateUsed( - response, - "account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION, - ) - self.assertTrue(response.context_data["token_fail"]) - - # When in XHR views, it should respond with a 400 bad request - # code, and the response body should contain the JSON-encoded - # error from the adapter - response = self.client.post( - url, - {"password1": "newpass123", "password2": "newpass123"}, - HTTP_X_REQUESTED_WITH="XMLHttpRequest", - ) - self.assertEqual(response.status_code, 400) - data = json.loads(response.content.decode("utf8")) - assert "invalid" in data["form"]["errors"][0] - @override_settings( ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.EMAIL ) @@ -296,3 +257,103 @@ def _create_user_and_login(self, usable_password=True): user = self._create_user(password=password) self.client.force_login(user) return user + + +def test_password_reset_flow(client, user, mailoutbox, settings): + """ + Tests the password reset flow: requesting a new password, + receiving the reset link via email and finally resetting the + password to a new value. + """ + settings.ACCOUNT_EMAIL_NOTIFICATIONS = True + + # Request new password + client.post( + reverse("account_reset_password"), + data={"email": user.email}, + ) + assert len(mail.outbox) == 1 + assert mailoutbox[0].to == [user.email] + body = mailoutbox[0].body + assert body.find("http://") > 0 + + # Extract URL for `password_reset_from_key` view and access it + url = body[body.find("/password/reset/") :].split()[0] + resp = client.get(url) + # Follow the redirect the actual password reset page with the key + # hidden. + url = resp.url + resp = client.get(url) + assertTemplateUsed( + resp, + "account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION, + ) + assert "token_fail" not in resp.context_data + + # Reset the password + resp = client.post(url, {"password1": "newpass123", "password2": "newpass123"}) + assertRedirects(resp, reverse("account_reset_password_from_key_done")) + assert "Your password has been reset" in mailoutbox[-1].body + + # Check the new password is in effect + user = get_user_model().objects.get(pk=user.pk) + assert user.check_password("newpass123") + + # Trying to reset the password against the same URL (or any other + # invalid/obsolete URL) returns a bad token response + resp = client.post(url, {"password1": "newpass123", "password2": "newpass123"}) + assertTemplateUsed( + resp, + "account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION, + ) + assert resp.context_data["token_fail"] + + # Same should happen when accessing the page directly + response = client.get(url) + assertTemplateUsed( + response, + "account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION, + ) + assert response.context_data["token_fail"] + + # When in XHR views, it should respond with a 400 bad request + # code, and the response body should contain the JSON-encoded + # error from the adapter + response = client.post( + url, + {"password1": "newpass123", "password2": "newpass123"}, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + assert response.status_code == 400 + data = json.loads(response.content.decode("utf8")) + assert "invalid" in data["form"]["errors"][0] + + +@pytest.mark.parametrize( + "next_url,expected_location", + [(None, reverse("account_reset_password_from_key_done")), ("/foo", "/foo")], +) +def test_reset_password_from_key_next_url( + user, client, password_factory, next_url, expected_location, password_reset_url +): + url = password_reset_url(user) + query = "" + if next_url: + query = "?" + urlencode({"next": next_url}) + resp = client.get(url + query) + assert resp.status_code == 302 + assert ( + resp["location"] + == reverse( + "account_reset_password_from_key", + kwargs={"uidb36": user_pk_to_url_str(user), "key": "set-password"}, + ) + + query + ) + password = password_factory() + data = {"password1": password, "password2": password} + if next_url: + data["next"] = next_url + resp = client.post(resp["location"], data) + assert resp.status_code == 302 + assert resp["location"] == expected_location diff --git a/allauth/account/utils.py b/allauth/account/utils.py index b9c179114f..4c29381fc3 100644 --- a/allauth/account/utils.py +++ b/allauth/account/utils.py @@ -4,7 +4,7 @@ from django.conf import settings from django.contrib import messages -from django.contrib.auth import get_user_model +from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model from django.core.exceptions import FieldDoesNotExist from django.db import models from django.db.models import Q @@ -34,7 +34,7 @@ def _unicode_ci_compare(s1, s2): return norm_s1 == norm_s2 -def get_next_redirect_url(request, redirect_field_name="next"): +def get_next_redirect_url(request, redirect_field_name=REDIRECT_FIELD_NAME): """ Returns the next URL to redirect to, if it was explicitly passed via the request. @@ -45,7 +45,9 @@ def get_next_redirect_url(request, redirect_field_name="next"): return redirect_to -def get_login_redirect_url(request, url=None, redirect_field_name="next", signup=False): +def get_login_redirect_url( + request, url=None, redirect_field_name=REDIRECT_FIELD_NAME, signup=False +): ret = url if url and callable(url): # In order to be able to pass url getters around that depend diff --git a/allauth/account/views.py b/allauth/account/views.py index 80225b41de..c882ec2c6d 100644 --- a/allauth/account/views.py +++ b/allauth/account/views.py @@ -1,21 +1,15 @@ from django.contrib import messages -from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.decorators import login_required from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import PermissionDenied from django.core.validators import validate_email from django.forms import ValidationError -from django.http import ( - Http404, - HttpResponse, - HttpResponsePermanentRedirect, - HttpResponseRedirect, -) +from django.http import Http404, HttpResponse, HttpResponseRedirect from django.urls import reverse, reverse_lazy from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from django.views.decorators.debug import sensitive_post_parameters -from django.views.generic.base import TemplateResponseMixin, TemplateView, View +from django.views.generic.base import TemplateView from django.views.generic.edit import FormView from allauth import app_settings as allauth_app_settings @@ -33,6 +27,14 @@ UserTokenForm, ) from allauth.account.internal import flows +from allauth.account.mixins import ( + AjaxCapableProcessFormViewMixin, + CloseableSignupMixin, + LogoutFunctionalityMixin, + NextRedirectMixin, + RedirectAuthenticatedUserMixin, + _ajax_response, +) from allauth.account.models import ( EmailAddress, EmailConfirmation, @@ -41,9 +43,6 @@ from allauth.account.reauthentication import resume_request from allauth.account.utils import ( complete_signup, - get_login_redirect_url, - get_next_redirect_url, - passthrough_next_redirect_url, perform_login, send_email_confirmation, sync_user_email_addresses, @@ -54,7 +53,7 @@ from allauth.core.exceptions import ImmediateHttpResponse from allauth.core.internal.httpkit import redirect from allauth.decorators import rate_limit -from allauth.utils import get_form_class, get_request_param +from allauth.utils import get_form_class INTERNAL_RESET_SESSION_KEY = "_password_reset_key" @@ -65,101 +64,23 @@ ) -def _ajax_response(request, response, form=None, data=None): - adapter = get_adapter() - if adapter.is_ajax(request): - if isinstance(response, HttpResponseRedirect) or isinstance( - response, HttpResponsePermanentRedirect - ): - redirect_to = response["Location"] - else: - redirect_to = None - response = adapter.ajax_response( - request, response, form=form, data=data, redirect_to=redirect_to - ) - return response - - -class RedirectAuthenticatedUserMixin(object): - def dispatch(self, request, *args, **kwargs): - if request.user.is_authenticated and app_settings.AUTHENTICATED_LOGIN_REDIRECTS: - redirect_to = self.get_authenticated_redirect_url() - response = HttpResponseRedirect(redirect_to) - return _ajax_response(request, response) - else: - response = super(RedirectAuthenticatedUserMixin, self).dispatch( - request, *args, **kwargs - ) - return response - - def get_authenticated_redirect_url(self): - redirect_field_name = self.redirect_field_name - return get_login_redirect_url( - self.request, - url=self.get_success_url(), - redirect_field_name=redirect_field_name, - ) - - -class AjaxCapableProcessFormViewMixin(object): - def get(self, request, *args, **kwargs): - response = super(AjaxCapableProcessFormViewMixin, self).get( - request, *args, **kwargs - ) - form = self.get_form() - return _ajax_response( - self.request, response, form=form, data=self._get_ajax_data_if() - ) - - def post(self, request, *args, **kwargs): - form_class = self.get_form_class() - form = self.get_form(form_class) - if form.is_valid(): - response = self.form_valid(form) - else: - response = self.form_invalid(form) - return _ajax_response( - self.request, response, form=form, data=self._get_ajax_data_if() - ) - - def get_form(self, form_class=None): - form = getattr(self, "_cached_form", None) - if form is None: - form = super(AjaxCapableProcessFormViewMixin, self).get_form(form_class) - self._cached_form = form - return form - - def _get_ajax_data_if(self): - return ( - self.get_ajax_data() - if get_adapter(self.request).is_ajax(self.request) - else None - ) - - def get_ajax_data(self): - return None - - -class LogoutFunctionalityMixin(object): - def logout(self): - flows.logout.logout(self.request) - - class LoginView( - RedirectAuthenticatedUserMixin, AjaxCapableProcessFormViewMixin, FormView + NextRedirectMixin, + RedirectAuthenticatedUserMixin, + AjaxCapableProcessFormViewMixin, + FormView, ): form_class = LoginForm template_name = "account/login." + app_settings.TEMPLATE_EXTENSION success_url = None - redirect_field_name = REDIRECT_FIELD_NAME @sensitive_post_parameters_m @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): - return super(LoginView, self).dispatch(request, *args, **kwargs) + return super().dispatch(request, *args, **kwargs) def get_form_kwargs(self): - kwargs = super(LoginView, self).get_form_kwargs() + kwargs = super().get_form_kwargs() kwargs["request"] = self.request return kwargs @@ -167,34 +88,21 @@ def get_form_class(self): return get_form_class(app_settings.FORMS, "login", self.form_class) def form_valid(self, form): - success_url = self.get_success_url() + redirect_url = self.get_success_url() try: - return form.login(self.request, redirect_url=success_url) + return form.login(self.request, redirect_url=redirect_url) except ImmediateHttpResponse as e: return e.response - def get_success_url(self): - # Explicitly passed ?next= URL takes precedence - ret = ( - get_next_redirect_url(self.request, self.redirect_field_name) - or self.success_url - ) - return ret - def get_context_data(self, **kwargs): ret = super(LoginView, self).get_context_data(**kwargs) - signup_url = passthrough_next_redirect_url( - self.request, reverse("account_signup"), self.redirect_field_name - ) - redirect_field_value = get_request_param(self.request, self.redirect_field_name) + signup_url = self.passthrough_next_url(reverse("account_signup")) site = get_current_site(self.request) ret.update( { "signup_url": signup_url, "site": site, - "redirect_field_name": self.redirect_field_name, - "redirect_field_value": redirect_field_value, "SOCIALACCOUNT_ENABLED": allauth_app_settings.SOCIALACCOUNT_ENABLED, } ) @@ -204,74 +112,42 @@ def get_context_data(self, **kwargs): login = LoginView.as_view() -class CloseableSignupMixin(object): - template_name_signup_closed = ( - "account/signup_closed." + app_settings.TEMPLATE_EXTENSION - ) - - def dispatch(self, request, *args, **kwargs): - try: - if not self.is_open(): - return self.closed() - except ImmediateHttpResponse as e: - return e.response - return super(CloseableSignupMixin, self).dispatch(request, *args, **kwargs) - - def is_open(self): - return get_adapter(self.request).is_open_for_signup(self.request) - - def closed(self): - response_kwargs = { - "request": self.request, - "template": self.template_name_signup_closed, - } - return self.response_class(**response_kwargs) - - @method_decorator(rate_limit(action="signup"), name="dispatch") class SignupView( RedirectAuthenticatedUserMixin, CloseableSignupMixin, + NextRedirectMixin, AjaxCapableProcessFormViewMixin, FormView, ): template_name = "account/signup." + app_settings.TEMPLATE_EXTENSION form_class = SignupForm - redirect_field_name = REDIRECT_FIELD_NAME - success_url = None @sensitive_post_parameters_m @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): - return super(SignupView, self).dispatch(request, *args, **kwargs) + return super().dispatch(request, *args, **kwargs) def get_form_class(self): return get_form_class(app_settings.FORMS, "signup", self.form_class) - def get_success_url(self): - # Explicitly passed ?next= URL takes precedence - ret = ( - get_next_redirect_url(self.request, self.redirect_field_name) - or self.success_url - ) - return ret - def form_valid(self, form): self.user, resp = form.try_save(self.request) if resp: return resp try: + redirect_url = self.get_success_url() return complete_signup( self.request, self.user, email_verification=None, - success_url=self.get_success_url(), + success_url=redirect_url, ) except ImmediateHttpResponse as e: return e.response def get_context_data(self, **kwargs): - ret = super(SignupView, self).get_context_data(**kwargs) + ret = super().get_context_data(**kwargs) form = ret["form"] email = self.request.session.get("account_verified_email") if email: @@ -280,17 +156,11 @@ def get_context_data(self, **kwargs): email_keys.append("email2") for email_key in email_keys: form.fields[email_key].initial = email - login_url = passthrough_next_redirect_url( - self.request, reverse("account_login"), self.redirect_field_name - ) - redirect_field_name = self.redirect_field_name + login_url = self.passthrough_next_url(reverse("account_login")) site = get_current_site(self.request) - redirect_field_value = get_request_param(self.request, redirect_field_name) ret.update( { "login_url": login_url, - "redirect_field_name": redirect_field_name, - "redirect_field_value": redirect_field_value, "site": site, "SOCIALACCOUNT_ENABLED": allauth_app_settings.SOCIALACCOUNT_ENABLED, } @@ -314,7 +184,7 @@ def get_initial(self): signup = SignupView.as_view() -class ConfirmEmailView(TemplateResponseMixin, LogoutFunctionalityMixin, View): +class ConfirmEmailView(NextRedirectMixin, LogoutFunctionalityMixin, TemplateView): template_name = "account/email_confirm." + app_settings.TEMPLATE_EXTENSION def get(self, *args, **kwargs): @@ -447,7 +317,7 @@ def get_ajax_data(self): return ret def get_context_data(self, **kwargs): - ctx = kwargs + ctx = super().get_context_data(**kwargs) site = get_current_site(self.request) ctx.update( { @@ -462,9 +332,12 @@ def get_context_data(self, **kwargs): return ctx def get_redirect_url(self): - return get_adapter(self.request).get_email_confirmation_redirect_url( - self.request - ) + url = self.get_next_url() + if not url: + url = get_adapter(self.request).get_email_confirmation_redirect_url( + self.request + ) + return url confirm_email = ConfirmEmailView.as_view() @@ -596,7 +469,7 @@ def get_ajax_data(self): @method_decorator(login_required, name="dispatch") @method_decorator(rate_limit(action="change_password"), name="dispatch") -class PasswordChangeView(AjaxCapableProcessFormViewMixin, FormView): +class PasswordChangeView(AjaxCapableProcessFormViewMixin, NextRedirectMixin, FormView): template_name = "account/password_change." + app_settings.TEMPLATE_EXTENSION form_class = ChangePasswordForm @@ -614,9 +487,7 @@ def get_form_kwargs(self): kwargs["user"] = self.request.user return kwargs - def get_success_url(self): - if self.success_url: - return self.success_url + def get_default_success_url(self): return get_adapter().get_password_change_redirect_url(self.request) def form_valid(self, form): @@ -642,7 +513,7 @@ def get_context_data(self, **kwargs): rate_limit(action="change_password"), name="dispatch", ) -class PasswordSetView(AjaxCapableProcessFormViewMixin, FormView): +class PasswordSetView(AjaxCapableProcessFormViewMixin, NextRedirectMixin, FormView): template_name = "account/password_set." + app_settings.TEMPLATE_EXTENSION form_class = SetPasswordForm @@ -660,9 +531,7 @@ def get_form_kwargs(self): kwargs["user"] = self.request.user return kwargs - def get_success_url(self): - if self.success_url: - return self.success_url + def get_default_success_url(self): return get_adapter().get_password_change_redirect_url(self.request) def form_valid(self, form): @@ -681,11 +550,10 @@ def get_context_data(self, **kwargs): password_set = PasswordSetView.as_view() -class PasswordResetView(AjaxCapableProcessFormViewMixin, FormView): +class PasswordResetView(NextRedirectMixin, AjaxCapableProcessFormViewMixin, FormView): template_name = "account/password_reset." + app_settings.TEMPLATE_EXTENSION form_class = ResetPasswordForm success_url = reverse_lazy("account_reset_password_done") - redirect_field_name = REDIRECT_FIELD_NAME def get_form_class(self): return get_form_class(app_settings.FORMS, "reset_password", self.form_class) @@ -699,13 +567,11 @@ def form_valid(self, form): if r429: return r429 form.save(self.request) - return super(PasswordResetView, self).form_valid(form) + return super().form_valid(form) def get_context_data(self, **kwargs): - ret = super(PasswordResetView, self).get_context_data(**kwargs) - login_url = passthrough_next_redirect_url( - self.request, reverse("account_login"), self.redirect_field_name - ) + ret = super().get_context_data(**kwargs) + login_url = self.passthrough_next_url(reverse("account_login")) # NOTE: For backwards compatibility ret["password_reset_form"] = ret.get("form") # (end NOTE) @@ -725,7 +591,10 @@ class PasswordResetDoneView(TemplateView): @method_decorator(rate_limit(action="reset_password_from_key"), name="dispatch") class PasswordResetFromKeyView( - AjaxCapableProcessFormViewMixin, LogoutFunctionalityMixin, FormView + AjaxCapableProcessFormViewMixin, + NextRedirectMixin, + LogoutFunctionalityMixin, + FormView, ): template_name = "account/password_reset_from_key." + app_settings.TEMPLATE_EXTENSION form_class = ResetPasswordKeyForm @@ -763,9 +632,7 @@ def dispatch(self, request, uidb36, key, **kwargs): self.logout() self.request.session[INTERNAL_RESET_SESSION_KEY] = self.key - return super(PasswordResetFromKeyView, self).dispatch( - request, uidb36, self.key, **kwargs - ) + return super().dispatch(request, uidb36, self.key, **kwargs) else: token_form = user_token_form_class(data={"uidb36": uidb36, "key": self.key}) if token_form.is_valid(): @@ -774,7 +641,9 @@ def dispatch(self, request, uidb36, key, **kwargs): # avoids the possibility of leaking the key in the # HTTP Referer header. self.request.session[INTERNAL_RESET_SESSION_KEY] = self.key - redirect_url = self.request.path.replace(self.key, self.reset_url_key) + redirect_url = self.passthrough_next_url( + self.request.path.replace(self.key, self.reset_url_key) + ) return redirect(redirect_url) self.reset_user = None @@ -821,9 +690,8 @@ class PasswordResetFromKeyDoneView(TemplateView): password_reset_from_key_done = PasswordResetFromKeyDoneView.as_view() -class LogoutView(TemplateResponseMixin, LogoutFunctionalityMixin, View): +class LogoutView(NextRedirectMixin, LogoutFunctionalityMixin, TemplateView): template_name = "account/logout." + app_settings.TEMPLATE_EXTENSION - redirect_field_name = REDIRECT_FIELD_NAME def get(self, *args, **kwargs): if app_settings.LOGOUT_ON_GET: @@ -842,21 +710,10 @@ def post(self, *args, **kwargs): response = redirect(url) return _ajax_response(self.request, response) - def get_context_data(self, **kwargs): - ctx = kwargs - redirect_field_value = get_request_param(self.request, self.redirect_field_name) - ctx.update( - { - "redirect_field_name": self.redirect_field_name, - "redirect_field_value": redirect_field_value, - } - ) - return ctx - def get_redirect_url(self): - return get_next_redirect_url( - self.request, self.redirect_field_name - ) or get_adapter(self.request).get_logout_redirect_url(self.request) + return self.get_next_url() or get_adapter(self.request).get_logout_redirect_url( + self.request + ) logout = LogoutView.as_view() @@ -876,9 +733,7 @@ class EmailVerificationSentView(TemplateView): email_verification_sent = EmailVerificationSentView.as_view() -class BaseReauthenticateView(FormView): - redirect_field_name = REDIRECT_FIELD_NAME - +class BaseReauthenticateView(NextRedirectMixin, FormView): def dispatch(self, request, *args, **kwargs): resp = self._check_reauthentication_method_available(request) if resp: @@ -903,15 +758,11 @@ def _check_reauthentication_method_available(self, request): if not methods: # Reauthentication not available raise PermissionDenied("Reauthentication not available") - url = passthrough_next_redirect_url( - request, methods[0]["url"], self.redirect_field_name - ) + url = self.passthrough_next_url(methods[0]["url"]) return HttpResponseRedirect(url) - def get_success_url(self): - url = get_next_redirect_url(self.request, self.redirect_field_name) - if not url: - url = get_adapter(self.request).get_login_redirect_url(self.request) + def get_default_success_url(self): + url = get_adapter(self.request).get_login_redirect_url(self.request) return url def form_valid(self, form): @@ -922,11 +773,8 @@ def form_valid(self, form): def get_context_data(self, **kwargs): ret = super().get_context_data(**kwargs) - redirect_field_value = get_request_param(self.request, self.redirect_field_name) ret.update( { - "redirect_field_name": self.redirect_field_name, - "redirect_field_value": redirect_field_value, "reauthentication_alternatives": self.get_reauthentication_alternatives(), } ) @@ -939,9 +787,7 @@ def get_reauthentication_alternatives(self): alt = dict(method) if self.request.path == alt["url"]: continue - alt["url"] = passthrough_next_redirect_url( - self.request, alt["url"], self.redirect_field_name - ) + alt["url"] = self.passthrough_next_url(alt["url"]) alts.append(alt) alts = sorted(alts, key=lambda alt: alt["description"]) return alts diff --git a/allauth/socialaccount/providers/facebook/provider.py b/allauth/socialaccount/providers/facebook/provider.py index 8f0bb0f507..0a4417edb6 100644 --- a/allauth/socialaccount/providers/facebook/provider.py +++ b/allauth/socialaccount/providers/facebook/provider.py @@ -3,6 +3,7 @@ import string from urllib.parse import quote +from django.contrib.auth import REDIRECT_FIELD_NAME from django.middleware.csrf import get_token from django.template.loader import render_to_string from django.urls import reverse @@ -70,7 +71,7 @@ def get_method(self): def get_login_url(self, request, **kwargs): method = kwargs.pop("method", self.get_method()) if method == "js_sdk": - next = "'%s'" % escapejs(kwargs.get("next") or "") + next = "'%s'" % escapejs(kwargs.get(REDIRECT_FIELD_NAME) or "") process = "'%s'" % escapejs(kwargs.get("process") or AuthProcess.LOGIN) action = "'%s'" % escapejs(kwargs.get("action") or AuthAction.AUTHENTICATE) scope = "'%s'" % escapejs(kwargs.get("scope", "")) diff --git a/allauth/socialaccount/providers/openid/views.py b/allauth/socialaccount/providers/openid/views.py index f9db6f1a17..a1de59e510 100644 --- a/allauth/socialaccount/providers/openid/views.py +++ b/allauth/socialaccount/providers/openid/views.py @@ -1,3 +1,4 @@ +from django.contrib.auth import REDIRECT_FIELD_NAME from django.http import HttpResponseRedirect from django.shortcuts import render from django.urls import reverse @@ -64,7 +65,7 @@ def get_form(self): if self.request.method == "GET" and "openid" not in self.request.GET: return self.form_class( initial={ - "next": self.request.GET.get("next"), + "next": self.request.GET.get(REDIRECT_FIELD_NAME), "process": self.request.GET.get("process"), } ) diff --git a/allauth/socialaccount/templatetags/socialaccount.py b/allauth/socialaccount/templatetags/socialaccount.py index 928544b606..74acc4b76c 100644 --- a/allauth/socialaccount/templatetags/socialaccount.py +++ b/allauth/socialaccount/templatetags/socialaccount.py @@ -1,4 +1,5 @@ from django import template +from django.contrib.auth import REDIRECT_FIELD_NAME from django.utils.safestring import mark_safe from allauth.socialaccount.adapter import get_adapter @@ -26,15 +27,15 @@ def provider_login_url(context, provider, **params): del query["scope"] if auth_params == "": del query["auth_params"] - if "next" not in query: - next = get_request_param(request, "next") + if REDIRECT_FIELD_NAME not in query: + next = get_request_param(request, REDIRECT_FIELD_NAME) if next: - query["next"] = next + query[REDIRECT_FIELD_NAME] = next elif process == "redirect": - query["next"] = request.get_full_path() + query[REDIRECT_FIELD_NAME] = request.get_full_path() else: - if not query["next"]: - del query["next"] + if not query[REDIRECT_FIELD_NAME]: + del query[REDIRECT_FIELD_NAME] # get the login url and append query as url parameters return provider.get_login_url(request, **query) diff --git a/allauth/templates/account/email_confirm.html b/allauth/templates/account/email_confirm.html index b34157429b..2020656a00 100644 --- a/allauth/templates/account/email_confirm.html +++ b/allauth/templates/account/email_confirm.html @@ -19,6 +19,7 @@ {% element form method="post" action=action_url %} {% slot actions %} {% csrf_token %} + {{ redirect_field }} {% element button type="submit" %} {% trans 'Confirm' %} {% endelement %} diff --git a/allauth/templates/account/login.html b/allauth/templates/account/login.html index 765c2fdd5e..d3519f8b4e 100644 --- a/allauth/templates/account/login.html +++ b/allauth/templates/account/login.html @@ -18,11 +18,7 @@ {% csrf_token %} {% element fields form=form unlabeled=True %} {% endelement %} - {% if redirect_field_value %} - - {% endif %} + {{ redirect_field }} {% endslot %} {% slot actions %} {% element button type="submit" tags="prominent,login" %} diff --git a/allauth/templates/account/logout.html b/allauth/templates/account/logout.html index 7c4adfa210..84530a923b 100644 --- a/allauth/templates/account/logout.html +++ b/allauth/templates/account/logout.html @@ -12,11 +12,7 @@ {% element form method="post" action=action_url no_visible_fields=True %} {% slot body %} {% csrf_token %} - {% if redirect_field_value %} - - {% endif %} + {{ redirect_field }} {% endslot %} {% slot actions %} {% element button type="submit" %} diff --git a/allauth/templates/account/password_change.html b/allauth/templates/account/password_change.html index 9df9e262b5..297dba6f13 100644 --- a/allauth/templates/account/password_change.html +++ b/allauth/templates/account/password_change.html @@ -11,6 +11,7 @@ {% element form form=form method="post" action=action_url %} {% slot body %} {% csrf_token %} + {{ redirect_field }} {% element fields form=form %} {% endelement %} {% endslot %} diff --git a/allauth/templates/account/password_reset_from_key.html b/allauth/templates/account/password_reset_from_key.html index f8f0d3f9bf..6c0ef3a5da 100644 --- a/allauth/templates/account/password_reset_from_key.html +++ b/allauth/templates/account/password_reset_from_key.html @@ -21,6 +21,7 @@ {% element form method="post" action=action_url %} {% slot body %} {% csrf_token %} + {{ redirect_field }} {% element fields form=form %} {% endelement %} {% endslot %} diff --git a/allauth/templates/account/password_set.html b/allauth/templates/account/password_set.html index 21e2fc822c..2cb77a4222 100644 --- a/allauth/templates/account/password_set.html +++ b/allauth/templates/account/password_set.html @@ -12,6 +12,7 @@ {% element form method="post" action=action_url %} {% slot body %} {% csrf_token %} + {{ redirect_field }} {% element fields form=form %} {% endelement %} {% endslot %} diff --git a/allauth/templates/account/reauthenticate.html b/allauth/templates/account/reauthenticate.html index 38c22def50..6d7fff5d99 100644 --- a/allauth/templates/account/reauthenticate.html +++ b/allauth/templates/account/reauthenticate.html @@ -9,11 +9,7 @@ {% csrf_token %} {% element fields form=form unlabeled=True %} {% endelement %} - {% if redirect_field_value %} - - {% endif %} + {{ redirect_field }} {% endslot %} {% slot actions %} {% element button type="submit" %} diff --git a/allauth/templates/account/signup.html b/allauth/templates/account/signup.html index d55fb146aa..d59c2553b3 100644 --- a/allauth/templates/account/signup.html +++ b/allauth/templates/account/signup.html @@ -16,11 +16,7 @@ {% csrf_token %} {% element fields form=form unlabeled=True %} {% endelement %} - {% if redirect_field_value %} - - {% endif %} + {{ redirect_field }} {% endslot %} {% slot actions %} {% element button tags="prominent,signup" type="submit" %} diff --git a/allauth/templates/mfa/reauthenticate.html b/allauth/templates/mfa/reauthenticate.html index 749af87af1..eca1dfe388 100644 --- a/allauth/templates/mfa/reauthenticate.html +++ b/allauth/templates/mfa/reauthenticate.html @@ -9,11 +9,7 @@ {% csrf_token %} {% element fields form=form unlabeled=True %} {% endelement %} - {% if redirect_field_value %} - - {% endif %} + {{ redirect_field }} {% endslot %} {% slot actions %} {% element button type="submit" tags="primary,mfa,login" %} diff --git a/allauth/templates/socialaccount/signup.html b/allauth/templates/socialaccount/signup.html index a361ec37df..cfcdfa88c0 100644 --- a/allauth/templates/socialaccount/signup.html +++ b/allauth/templates/socialaccount/signup.html @@ -18,11 +18,7 @@ {% csrf_token %} {% element fields form=form unlabeled=True %} {% endelement %} - {% if redirect_field_value %} - - {% endif %} + {{ redirect_field }} {% endslot %} {% slot actions %} {% element button type="submit" %}