Plain is headed towards 1.0! Subscribe for development updates →

  1from plain import forms
  2from plain.auth import get_user_model
  3from plain.exceptions import ValidationError
  4from plain.models.forms import ModelForm
  5
  6from .core import check_user_password
  7from .hashers import check_password
  8from .utils import unicode_ci_compare
  9
 10
 11class PasswordResetForm(forms.Form):
 12    email = forms.EmailField(max_length=254)
 13
 14    def send_mail(
 15        self,
 16        *,
 17        template_name: str,
 18        context: dict,
 19        from_email: str,
 20        to_email: str,
 21    ):
 22        from plain.email import TemplateEmail
 23
 24        email = TemplateEmail(
 25            template=template_name,
 26            context=context,
 27            from_email=from_email,
 28            to=[to_email],
 29            headers={
 30                "X-Auto-Response-Suppress": "All",
 31            },
 32        )
 33
 34        email.send()
 35
 36    def get_users(self, email):
 37        """Given an email, return matching user(s) who should receive a reset.
 38
 39        This allows subclasses to more easily customize the default policies
 40        that prevent inactive users and users with unusable passwords from
 41        resetting their password.
 42        """
 43        active_users = get_user_model()._default_manager.filter(email__iexact=email)
 44        return (u for u in active_users if unicode_ci_compare(email, u.email))
 45
 46    def save(
 47        self,
 48        *,
 49        generate_reset_url: callable,
 50        email_template_name: str = "password_reset",
 51        from_email: str = "",
 52        extra_email_context: dict | None = None,
 53    ):
 54        """
 55        Generate a one-use only link for resetting password and send it to the
 56        user.
 57        """
 58        email = self.cleaned_data["email"]
 59        for user in self.get_users(email):
 60            context = {
 61                "email": email,
 62                "user": user,
 63                "url": generate_reset_url(user),
 64                **(extra_email_context or {}),
 65            }
 66            self.send_mail(
 67                template_name=email_template_name,
 68                context=context,
 69                from_email=from_email,
 70                to_email=user.email,
 71            )
 72
 73
 74class PasswordSetForm(forms.Form):
 75    """
 76    A form that lets a user set their password without entering the old
 77    password
 78    """
 79
 80    new_password1 = forms.CharField(strip=False)
 81    new_password2 = forms.CharField(strip=False)
 82
 83    def __init__(self, user, *args, **kwargs):
 84        self.user = user
 85        super().__init__(*args, **kwargs)
 86
 87    def clean_new_password2(self):
 88        password1 = self.cleaned_data.get("new_password1")
 89        password2 = self.cleaned_data.get("new_password2")
 90        if password1 and password2 and password1 != password2:
 91            raise ValidationError(
 92                "The two password fields didn't match.",
 93                code="password_mismatch",
 94            )
 95
 96        # Clean it as if it were being put into the model directly
 97        self.user._meta.get_field("password").clean(password2, self.user)
 98
 99        return password2
100
101    def save(self, commit=True):
102        self.user.password = self.cleaned_data["new_password1"]
103        if commit:
104            self.user.save()
105        return self.user
106
107
108class PasswordChangeForm(PasswordSetForm):
109    """
110    A form that lets a user change their password by entering their old
111    password.
112    """
113
114    current_password = forms.CharField(strip=False)
115
116    def clean_current_password(self):
117        """
118        Validate that the current_password field is correct.
119        """
120        current_password = self.cleaned_data["current_password"]
121        if not check_user_password(self.user, current_password):
122            raise ValidationError(
123                "Your old password was entered incorrectly. Please enter it again.",
124                code="password_incorrect",
125            )
126        return current_password
127
128
129class PasswordLoginForm(forms.Form):
130    email = forms.EmailField(max_length=150)
131    password = forms.CharField(strip=False)
132
133    def clean(self):
134        User = get_user_model()
135
136        email = self.cleaned_data.get("email")
137        password = self.cleaned_data.get("password")
138
139        if email and password:
140            try:
141                # The vast majority of users won't have a case-sensitive email, so we act that way
142                user = User.objects.get(email__iexact=email)
143            except User.DoesNotExist:
144                # Run the default password hasher once to reduce the timing
145                # difference between an existing and a nonexistent user (django #20760).
146                check_password(password, "")
147
148                raise ValidationError(
149                    "Please enter a correct email and password. Note that both fields may be case-sensitive.",
150                    code="invalid_login",
151                )
152
153            if not check_user_password(user, password):
154                raise ValidationError(
155                    "Please enter a correct email and password. Note that both fields may be case-sensitive.",
156                    code="invalid_login",
157                )
158
159            self._user = user
160
161        return self.cleaned_data
162
163    def get_user(self):
164        return self._user
165
166
167class PasswordSignupForm(ModelForm):
168    confirm_password = forms.CharField(strip=False)
169
170    class Meta:
171        model = get_user_model()
172        fields = ("email", "password")
173
174    def clean(self):
175        cleaned_data = super().clean()
176        password = cleaned_data.get("password")
177        confirm_password = cleaned_data.get("confirm_password")
178        if password and confirm_password and password != confirm_password:
179            raise ValidationError(
180                "The two password fields didn't match.",
181                code="password_mismatch",
182            )
183        return cleaned_data