from __future__ import absolute_import import warnings from importlib import import_module from django import forms from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.sites.shortcuts import get_current_site from django.core import exceptions, validators from django.urls import reverse from django.utils.translation import gettext, gettext_lazy as _, pgettext from ..utils import ( build_absolute_uri, get_username_max_length, set_form_field_order, ) from . import app_settings from .adapter import get_adapter from .app_settings import AuthenticationMethod from .models import EmailAddress from .utils import ( filter_users_by_email, get_user_model, perform_login, setup_user_email, sync_user_email_addresses, url_str_to_user_pk, user_email, user_pk_to_url_str, user_username, ) class EmailAwarePasswordResetTokenGenerator(PasswordResetTokenGenerator): def _make_hash_value(self, user, timestamp): ret = super(EmailAwarePasswordResetTokenGenerator, self)._make_hash_value( user, timestamp ) sync_user_email_addresses(user) email = user_email(user) emails = set([email] if email else []) emails.update( EmailAddress.objects.filter(user=user).values_list("email", flat=True) ) ret += "|".join(sorted(emails)) return ret default_token_generator = EmailAwarePasswordResetTokenGenerator() class PasswordVerificationMixin(object): def clean(self): cleaned_data = super(PasswordVerificationMixin, self).clean() password1 = cleaned_data.get("password1") password2 = cleaned_data.get("password2") if (password1 and password2) and password1 != password2: self.add_error("password2", _("You must type the same password each time.")) return cleaned_data class PasswordField(forms.CharField): def __init__(self, *args, **kwargs): render_value = kwargs.pop( "render_value", app_settings.PASSWORD_INPUT_RENDER_VALUE ) kwargs["widget"] = forms.PasswordInput( render_value=render_value, attrs={"placeholder": kwargs.get("label")}, ) autocomplete = kwargs.pop("autocomplete", None) if autocomplete is not None: kwargs["widget"].attrs["autocomplete"] = autocomplete super(PasswordField, self).__init__(*args, **kwargs) class SetPasswordField(PasswordField): def __init__(self, *args, **kwargs): kwargs["autocomplete"] = "new-password" super(SetPasswordField, self).__init__(*args, **kwargs) self.user = None def clean(self, value): value = super(SetPasswordField, self).clean(value) value = get_adapter().clean_password(value, user=self.user) return value class LoginForm(forms.Form): password = PasswordField(label=_("Password"), autocomplete="current-password") remember = forms.BooleanField(label=_("Remember Me"), required=False) user = None error_messages = { "account_inactive": _("This account is currently inactive."), "email_password_mismatch": _( "The e-mail address and/or password you specified are not correct." ), "username_password_mismatch": _( "The username and/or password you specified are not correct." ), } def __init__(self, *args, **kwargs): self.request = kwargs.pop("request", None) super(LoginForm, self).__init__(*args, **kwargs) if app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.EMAIL: login_widget = forms.TextInput( attrs={ "type": "email", "placeholder": _("E-mail address"), "autocomplete": "email", } ) login_field = forms.EmailField(label=_("E-mail"), widget=login_widget) elif app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.USERNAME: login_widget = forms.TextInput( attrs={"placeholder": _("Username"), "autocomplete": "username"} ) login_field = forms.CharField( label=_("Username"), widget=login_widget, max_length=get_username_max_length(), ) else: assert ( app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.USERNAME_EMAIL ) login_widget = forms.TextInput( attrs={"placeholder": _("Username or e-mail"), "autocomplete": "email"} ) login_field = forms.CharField( label=pgettext("field label", "Login"), widget=login_widget ) self.fields["login"] = login_field set_form_field_order(self, ["login", "password", "remember"]) if app_settings.SESSION_REMEMBER is not None: del self.fields["remember"] def user_credentials(self): """ Provides the credentials required to authenticate the user for login. """ credentials = {} login = self.cleaned_data["login"] if app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.EMAIL: credentials["email"] = login elif app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.USERNAME: credentials["username"] = login else: if self._is_login_email(login): credentials["email"] = login credentials["username"] = login credentials["password"] = self.cleaned_data["password"] return credentials def clean_login(self): login = self.cleaned_data["login"] return login.strip() def _is_login_email(self, login): try: validators.validate_email(login) ret = True except exceptions.ValidationError: ret = False return ret def clean(self): super(LoginForm, self).clean() if self._errors: return credentials = self.user_credentials() user = get_adapter(self.request).authenticate(self.request, **credentials) if user: self.user = user else: auth_method = app_settings.AUTHENTICATION_METHOD if auth_method == app_settings.AuthenticationMethod.USERNAME_EMAIL: login = self.cleaned_data["login"] if self._is_login_email(login): auth_method = app_settings.AuthenticationMethod.EMAIL else: auth_method = app_settings.AuthenticationMethod.USERNAME raise forms.ValidationError( self.error_messages["%s_password_mismatch" % auth_method] ) return self.cleaned_data def login(self, request, redirect_url=None): email = self.user_credentials().get("email") ret = perform_login( request, self.user, email_verification=app_settings.EMAIL_VERIFICATION, redirect_url=redirect_url, email=email, ) remember = app_settings.SESSION_REMEMBER if remember is None: remember = self.cleaned_data["remember"] if remember: request.session.set_expiry(app_settings.SESSION_COOKIE_AGE) else: request.session.set_expiry(0) return ret class _DummyCustomSignupForm(forms.Form): def signup(self, request, user): """ Invoked at signup time to complete the signup of the user. """ pass def _base_signup_form_class(): """ Currently, we inherit from the custom form, if any. This is all not very elegant, though it serves a purpose: - There are two signup forms: one for local accounts, and one for social accounts - Both share a common base (BaseSignupForm) - Given the above, how to put in a custom signup form? Which form would your custom form derive from, the local or the social one? """ if not app_settings.SIGNUP_FORM_CLASS: return _DummyCustomSignupForm try: fc_module, fc_classname = app_settings.SIGNUP_FORM_CLASS.rsplit(".", 1) except ValueError: raise exceptions.ImproperlyConfigured( "%s does not point to a form class" % app_settings.SIGNUP_FORM_CLASS ) try: mod = import_module(fc_module) except ImportError as e: raise exceptions.ImproperlyConfigured( "Error importing form class %s:" ' "%s"' % (fc_module, e) ) try: fc_class = getattr(mod, fc_classname) except AttributeError: raise exceptions.ImproperlyConfigured( 'Module "%s" does not define a' ' "%s" class' % (fc_module, fc_classname) ) if not hasattr(fc_class, "signup"): if hasattr(fc_class, "save"): warnings.warn( "The custom signup form must offer" " a `def signup(self, request, user)` method", DeprecationWarning, ) else: raise exceptions.ImproperlyConfigured( 'The custom signup form must implement a "signup" method' ) return fc_class class BaseSignupForm(_base_signup_form_class()): username = forms.CharField( label=_("Username"), min_length=app_settings.USERNAME_MIN_LENGTH, widget=forms.TextInput( attrs={"placeholder": _("Username"), "autocomplete": "username"} ), ) email = forms.EmailField( widget=forms.TextInput( attrs={ "type": "email", "placeholder": _("E-mail address"), "autocomplete": "email", } ) ) def __init__(self, *args, **kwargs): email_required = kwargs.pop("email_required", app_settings.EMAIL_REQUIRED) self.username_required = kwargs.pop( "username_required", app_settings.USERNAME_REQUIRED ) super(BaseSignupForm, self).__init__(*args, **kwargs) username_field = self.fields["username"] username_field.max_length = get_username_max_length() username_field.validators.append( validators.MaxLengthValidator(username_field.max_length) ) username_field.widget.attrs["maxlength"] = str(username_field.max_length) default_field_order = [ "email", "email2", # ignored when not present "username", "password1", "password2", # ignored when not present ] if app_settings.SIGNUP_EMAIL_ENTER_TWICE: self.fields["email2"] = forms.EmailField( label=_("E-mail (again)"), widget=forms.TextInput( attrs={ "type": "email", "placeholder": _("E-mail address confirmation"), } ), ) if email_required: self.fields["email"].label = gettext("E-mail") self.fields["email"].required = True else: self.fields["email"].label = gettext("E-mail (optional)") self.fields["email"].required = False self.fields["email"].widget.is_required = False if self.username_required: default_field_order = [ "username", "email", "email2", # ignored when not present "password1", "password2", # ignored when not present ] if not self.username_required: del self.fields["username"] set_form_field_order( self, getattr(self, "field_order", None) or default_field_order ) def clean_username(self): value = self.cleaned_data["username"] value = get_adapter().clean_username(value) return value def clean_email(self): value = self.cleaned_data["email"] value = get_adapter().clean_email(value) if value and app_settings.UNIQUE_EMAIL: value = self.validate_unique_email(value) return value def validate_unique_email(self, value): return get_adapter().validate_unique_email(value) def clean(self): cleaned_data = super(BaseSignupForm, self).clean() if app_settings.SIGNUP_EMAIL_ENTER_TWICE: email = cleaned_data.get("email") email2 = cleaned_data.get("email2") if (email and email2) and email != email2: self.add_error("email2", _("You must type the same email each time.")) return cleaned_data def custom_signup(self, request, user): custom_form = super(BaseSignupForm, self) if hasattr(custom_form, "signup") and callable(custom_form.signup): custom_form.signup(request, user) else: warnings.warn( "The custom signup form must offer" " a `def signup(self, request, user)` method", DeprecationWarning, ) # Historically, it was called .save, but this is confusing # in case of ModelForm custom_form.save(user) class SignupForm(BaseSignupForm): def __init__(self, *args, **kwargs): super(SignupForm, self).__init__(*args, **kwargs) self.fields["password1"] = PasswordField( label=_("Password"), autocomplete="new-password" ) if app_settings.SIGNUP_PASSWORD_ENTER_TWICE: self.fields["password2"] = PasswordField( label=_("Password (again)"), autocomplete="new-password" ) if hasattr(self, "field_order"): set_form_field_order(self, self.field_order) def clean(self): super(SignupForm, self).clean() # `password` cannot be of type `SetPasswordField`, as we don't # have a `User` yet. So, let's populate a dummy user to be used # for password validaton. User = get_user_model() dummy_user = User() user_username(dummy_user, self.cleaned_data.get("username")) user_email(dummy_user, self.cleaned_data.get("email")) password = self.cleaned_data.get("password1") if password: try: get_adapter().clean_password(password, user=dummy_user) except forms.ValidationError as e: self.add_error("password1", e) if ( app_settings.SIGNUP_PASSWORD_ENTER_TWICE and "password1" in self.cleaned_data and "password2" in self.cleaned_data ): if self.cleaned_data["password1"] != self.cleaned_data["password2"]: self.add_error( "password2", _("You must type the same password each time."), ) return self.cleaned_data def save(self, request): adapter = get_adapter(request) user = adapter.new_user(request) adapter.save_user(request, user, self) self.custom_signup(request, user) # TODO: Move into adapter `save_user` ? setup_user_email(request, user, []) return user class UserForm(forms.Form): def __init__(self, user=None, *args, **kwargs): self.user = user super(UserForm, self).__init__(*args, **kwargs) class AddEmailForm(UserForm): email = forms.EmailField( label=_("E-mail"), required=True, widget=forms.TextInput( attrs={"type": "email", "placeholder": _("E-mail address")} ), ) def clean_email(self): value = self.cleaned_data["email"] value = get_adapter().clean_email(value) errors = { "this_account": _( "This e-mail address is already associated with this account." ), "different_account": _( "This e-mail address is already associated with another account." ), "max_email_addresses": _("You cannot add more than %d e-mail addresses."), } users = filter_users_by_email(value) on_this_account = [u for u in users if u.pk == self.user.pk] on_diff_account = [u for u in users if u.pk != self.user.pk] if on_this_account: raise forms.ValidationError(errors["this_account"]) if on_diff_account and app_settings.UNIQUE_EMAIL: raise forms.ValidationError(errors["different_account"]) if not EmailAddress.objects.can_add_email(self.user): raise forms.ValidationError( errors["max_email_addresses"] % app_settings.MAX_EMAIL_ADDRESSES ) return value def save(self, request): return EmailAddress.objects.add_email( request, self.user, self.cleaned_data["email"], confirm=True ) class ChangePasswordForm(PasswordVerificationMixin, UserForm): oldpassword = PasswordField( label=_("Current Password"), autocomplete="current-password" ) password1 = SetPasswordField(label=_("New Password")) password2 = PasswordField(label=_("New Password (again)")) def __init__(self, *args, **kwargs): super(ChangePasswordForm, self).__init__(*args, **kwargs) self.fields["password1"].user = self.user def clean_oldpassword(self): if not self.user.check_password(self.cleaned_data.get("oldpassword")): raise forms.ValidationError(_("Please type your current password.")) return self.cleaned_data["oldpassword"] def save(self): get_adapter().set_password(self.user, self.cleaned_data["password1"]) class SetPasswordForm(PasswordVerificationMixin, UserForm): password1 = SetPasswordField(label=_("Password")) password2 = PasswordField(label=_("Password (again)")) def __init__(self, *args, **kwargs): super(SetPasswordForm, self).__init__(*args, **kwargs) self.fields["password1"].user = self.user def save(self): get_adapter().set_password(self.user, self.cleaned_data["password1"]) class ResetPasswordForm(forms.Form): email = forms.EmailField( label=_("E-mail"), required=True, widget=forms.TextInput( attrs={ "type": "email", "placeholder": _("E-mail address"), "autocomplete": "email", } ), ) def clean_email(self): email = self.cleaned_data["email"] email = get_adapter().clean_email(email) self.users = filter_users_by_email(email, is_active=True) if not self.users and not app_settings.PREVENT_ENUMERATION: raise forms.ValidationError( _("The e-mail address is not assigned to any user account") ) return self.cleaned_data["email"] def save(self, request, **kwargs): email = self.cleaned_data["email"] if not self.users: self._send_unknown_account_mail(request, email) else: self._send_password_reset_mail(request, email, self.users, **kwargs) return email def _send_unknown_account_mail(self, request, email): signup_url = build_absolute_uri(request, reverse("account_signup")) context = { "current_site": get_current_site(request), "email": email, "request": request, "signup_url": signup_url, } get_adapter(request).send_mail("account/email/unknown_account", email, context) def _send_password_reset_mail(self, request, email, users, **kwargs): token_generator = kwargs.get("token_generator", default_token_generator) for user in users: temp_key = token_generator.make_token(user) # save it to the password reset model # password_reset = PasswordReset(user=user, temp_key=temp_key) # password_reset.save() # send the password reset email path = reverse( "account_reset_password_from_key", kwargs=dict(uidb36=user_pk_to_url_str(user), key=temp_key), ) url = build_absolute_uri(request, path) context = { "current_site": get_current_site(request), "user": user, "password_reset_url": url, "request": request, } if app_settings.AUTHENTICATION_METHOD != AuthenticationMethod.EMAIL: context["username"] = user_username(user) get_adapter(request).send_mail( "account/email/password_reset_key", email, context ) class ResetPasswordKeyForm(PasswordVerificationMixin, forms.Form): password1 = SetPasswordField(label=_("New Password")) password2 = PasswordField(label=_("New Password (again)")) def __init__(self, *args, **kwargs): self.user = kwargs.pop("user", None) self.temp_key = kwargs.pop("temp_key", None) super(ResetPasswordKeyForm, self).__init__(*args, **kwargs) self.fields["password1"].user = self.user def save(self): get_adapter().set_password(self.user, self.cleaned_data["password1"]) class UserTokenForm(forms.Form): uidb36 = forms.CharField() key = forms.CharField() reset_user = None token_generator = default_token_generator error_messages = { "token_invalid": _("The password reset token was invalid."), } def _get_user(self, uidb36): User = get_user_model() try: pk = url_str_to_user_pk(uidb36) return User.objects.get(pk=pk) except (ValueError, User.DoesNotExist): return None def clean(self): cleaned_data = super(UserTokenForm, self).clean() uidb36 = cleaned_data.get("uidb36", None) key = cleaned_data.get("key", None) if not key: raise forms.ValidationError(self.error_messages["token_invalid"]) self.reset_user = self._get_user(uidb36) if self.reset_user is None or not self.token_generator.check_token( self.reset_user, key ): raise forms.ValidationError(self.error_messages["token_invalid"]) return cleaned_data