123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- import unicodedata
-
- from django import forms
- from django.contrib.auth import (
- authenticate, get_user_model, password_validation,
- )
- from django.contrib.auth.hashers import (
- UNUSABLE_PASSWORD_PREFIX, identify_hasher,
- )
- from django.contrib.auth.models import User
- from django.contrib.auth.tokens import default_token_generator
- from django.contrib.sites.shortcuts import get_current_site
- from django.core.mail import EmailMultiAlternatives
- from django.template import loader
- from django.utils.encoding import force_bytes
- from django.utils.http import urlsafe_base64_encode
- from django.utils.text import capfirst
- from django.utils.translation import gettext, gettext_lazy as _
-
- UserModel = get_user_model()
-
-
- class ReadOnlyPasswordHashWidget(forms.Widget):
- template_name = 'auth/widgets/read_only_password_hash.html'
- read_only = True
-
- def get_context(self, name, value, attrs):
- context = super().get_context(name, value, attrs)
- summary = []
- if not value or value.startswith(UNUSABLE_PASSWORD_PREFIX):
- summary.append({'label': gettext("No password set.")})
- else:
- try:
- hasher = identify_hasher(value)
- except ValueError:
- summary.append({'label': gettext("Invalid password format or unknown hashing algorithm.")})
- else:
- for key, value_ in hasher.safe_summary(value).items():
- summary.append({'label': gettext(key), 'value': value_})
- context['summary'] = summary
- return context
-
-
- class ReadOnlyPasswordHashField(forms.Field):
- widget = ReadOnlyPasswordHashWidget
-
- def __init__(self, *args, **kwargs):
- kwargs.setdefault("required", False)
- super().__init__(*args, **kwargs)
-
- def bound_data(self, data, initial):
- # Always return initial because the widget doesn't
- # render an input field.
- return initial
-
- def has_changed(self, initial, data):
- return False
-
-
- class UsernameField(forms.CharField):
- def to_python(self, value):
- return unicodedata.normalize('NFKC', super().to_python(value))
-
-
- class UserCreationForm(forms.ModelForm):
- """
- A form that creates a user, with no privileges, from the given username and
- password.
- """
- error_messages = {
- 'password_mismatch': _("The two password fields didn't match."),
- }
- password1 = forms.CharField(
- label=_("Password"),
- strip=False,
- widget=forms.PasswordInput,
- help_text=password_validation.password_validators_help_text_html(),
- )
- password2 = forms.CharField(
- label=_("Password confirmation"),
- widget=forms.PasswordInput,
- strip=False,
- help_text=_("Enter the same password as before, for verification."),
- )
-
- class Meta:
- model = User
- fields = ("username",)
- field_classes = {'username': UsernameField}
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- if self._meta.model.USERNAME_FIELD in self.fields:
- self.fields[self._meta.model.USERNAME_FIELD].widget.attrs.update({'autofocus': True})
-
- def clean_password2(self):
- password1 = self.cleaned_data.get("password1")
- password2 = self.cleaned_data.get("password2")
- if password1 and password2 and password1 != password2:
- raise forms.ValidationError(
- self.error_messages['password_mismatch'],
- code='password_mismatch',
- )
- return password2
-
- def _post_clean(self):
- super()._post_clean()
- # Validate the password after self.instance is updated with form data
- # by super().
- password = self.cleaned_data.get('password2')
- if password:
- try:
- password_validation.validate_password(password, self.instance)
- except forms.ValidationError as error:
- self.add_error('password2', error)
-
- def save(self, commit=True):
- user = super().save(commit=False)
- user.set_password(self.cleaned_data["password1"])
- if commit:
- user.save()
- return user
-
-
- class UserChangeForm(forms.ModelForm):
- password = ReadOnlyPasswordHashField(
- label=_("Password"),
- help_text=_(
- "Raw passwords are not stored, so there is no way to see this "
- "user's password, but you can change the password using "
- "<a href=\"{}\">this form</a>."
- ),
- )
-
- class Meta:
- model = User
- fields = '__all__'
- field_classes = {'username': UsernameField}
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- password = self.fields.get('password')
- if password:
- password.help_text = password.help_text.format('../password/')
- user_permissions = self.fields.get('user_permissions')
- if user_permissions:
- user_permissions.queryset = user_permissions.queryset.select_related('content_type')
-
- def clean_password(self):
- # Regardless of what the user provides, return the initial value.
- # This is done here, rather than on the field, because the
- # field does not have access to the initial value
- return self.initial.get('password')
-
-
- class AuthenticationForm(forms.Form):
- """
- Base class for authenticating users. Extend this to get a form that accepts
- username/password logins.
- """
- username = UsernameField(widget=forms.TextInput(attrs={'autofocus': True}))
- password = forms.CharField(
- label=_("Password"),
- strip=False,
- widget=forms.PasswordInput,
- )
-
- error_messages = {
- 'invalid_login': _(
- "Please enter a correct %(username)s and password. Note that both "
- "fields may be case-sensitive."
- ),
- 'inactive': _("This account is inactive."),
- }
-
- def __init__(self, request=None, *args, **kwargs):
- """
- The 'request' parameter is set for custom auth use by subclasses.
- The form data comes in via the standard 'data' kwarg.
- """
- self.request = request
- self.user_cache = None
- super().__init__(*args, **kwargs)
-
- # Set the max length and label for the "username" field.
- self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
- self.fields['username'].max_length = self.username_field.max_length or 254
- if self.fields['username'].label is None:
- self.fields['username'].label = capfirst(self.username_field.verbose_name)
-
- def clean(self):
- username = self.cleaned_data.get('username')
- password = self.cleaned_data.get('password')
-
- if username is not None and password:
- self.user_cache = authenticate(self.request, username=username, password=password)
- if self.user_cache is None:
- raise self.get_invalid_login_error()
- else:
- self.confirm_login_allowed(self.user_cache)
-
- return self.cleaned_data
-
- def confirm_login_allowed(self, user):
- """
- Controls whether the given User may log in. This is a policy setting,
- independent of end-user authentication. This default behavior is to
- allow login by active users, and reject login by inactive users.
-
- If the given user cannot log in, this method should raise a
- ``forms.ValidationError``.
-
- If the given user may log in, this method should return None.
- """
- if not user.is_active:
- raise forms.ValidationError(
- self.error_messages['inactive'],
- code='inactive',
- )
-
- def get_user(self):
- return self.user_cache
-
- def get_invalid_login_error(self):
- return forms.ValidationError(
- self.error_messages['invalid_login'],
- code='invalid_login',
- params={'username': self.username_field.verbose_name},
- )
-
-
- class PasswordResetForm(forms.Form):
- email = forms.EmailField(label=_("Email"), max_length=254)
-
- def send_mail(self, subject_template_name, email_template_name,
- context, from_email, to_email, html_email_template_name=None):
- """
- Send a django.core.mail.EmailMultiAlternatives to `to_email`.
- """
- subject = loader.render_to_string(subject_template_name, context)
- # Email subject *must not* contain newlines
- subject = ''.join(subject.splitlines())
- body = loader.render_to_string(email_template_name, context)
-
- email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
- if html_email_template_name is not None:
- html_email = loader.render_to_string(html_email_template_name, context)
- email_message.attach_alternative(html_email, 'text/html')
-
- email_message.send()
-
- def get_users(self, email):
- """Given an email, return matching user(s) who should receive a reset.
-
- This allows subclasses to more easily customize the default policies
- that prevent inactive users and users with unusable passwords from
- resetting their password.
- """
- active_users = UserModel._default_manager.filter(**{
- '%s__iexact' % UserModel.get_email_field_name(): email,
- 'is_active': True,
- })
- return (u for u in active_users if u.has_usable_password())
-
- def save(self, domain_override=None,
- subject_template_name='registration/password_reset_subject.txt',
- email_template_name='registration/password_reset_email.html',
- use_https=False, token_generator=default_token_generator,
- from_email=None, request=None, html_email_template_name=None,
- extra_email_context=None):
- """
- Generate a one-use only link for resetting password and send it to the
- user.
- """
- email = self.cleaned_data["email"]
- for user in self.get_users(email):
- if not domain_override:
- current_site = get_current_site(request)
- site_name = current_site.name
- domain = current_site.domain
- else:
- site_name = domain = domain_override
- context = {
- 'email': email,
- 'domain': domain,
- 'site_name': site_name,
- 'uid': urlsafe_base64_encode(force_bytes(user.pk)),
- 'user': user,
- 'token': token_generator.make_token(user),
- 'protocol': 'https' if use_https else 'http',
- **(extra_email_context or {}),
- }
- self.send_mail(
- subject_template_name, email_template_name, context, from_email,
- email, html_email_template_name=html_email_template_name,
- )
-
-
- class SetPasswordForm(forms.Form):
- """
- A form that lets a user change set their password without entering the old
- password
- """
- error_messages = {
- 'password_mismatch': _("The two password fields didn't match."),
- }
- new_password1 = forms.CharField(
- label=_("New password"),
- widget=forms.PasswordInput,
- strip=False,
- help_text=password_validation.password_validators_help_text_html(),
- )
- new_password2 = forms.CharField(
- label=_("New password confirmation"),
- strip=False,
- widget=forms.PasswordInput,
- )
-
- def __init__(self, user, *args, **kwargs):
- self.user = user
- super().__init__(*args, **kwargs)
-
- def clean_new_password2(self):
- password1 = self.cleaned_data.get('new_password1')
- password2 = self.cleaned_data.get('new_password2')
- if password1 and password2:
- if password1 != password2:
- raise forms.ValidationError(
- self.error_messages['password_mismatch'],
- code='password_mismatch',
- )
- password_validation.validate_password(password2, self.user)
- return password2
-
- def save(self, commit=True):
- password = self.cleaned_data["new_password1"]
- self.user.set_password(password)
- if commit:
- self.user.save()
- return self.user
-
-
- class PasswordChangeForm(SetPasswordForm):
- """
- A form that lets a user change their password by entering their old
- password.
- """
- error_messages = {
- **SetPasswordForm.error_messages,
- 'password_incorrect': _("Your old password was entered incorrectly. Please enter it again."),
- }
- old_password = forms.CharField(
- label=_("Old password"),
- strip=False,
- widget=forms.PasswordInput(attrs={'autofocus': True}),
- )
-
- field_order = ['old_password', 'new_password1', 'new_password2']
-
- def clean_old_password(self):
- """
- Validate that the old_password field is correct.
- """
- old_password = self.cleaned_data["old_password"]
- if not self.user.check_password(old_password):
- raise forms.ValidationError(
- self.error_messages['password_incorrect'],
- code='password_incorrect',
- )
- return old_password
-
-
- class AdminPasswordChangeForm(forms.Form):
- """
- A form used to change the password of a user in the admin interface.
- """
- error_messages = {
- 'password_mismatch': _("The two password fields didn't match."),
- }
- required_css_class = 'required'
- password1 = forms.CharField(
- label=_("Password"),
- widget=forms.PasswordInput(attrs={'autofocus': True}),
- strip=False,
- help_text=password_validation.password_validators_help_text_html(),
- )
- password2 = forms.CharField(
- label=_("Password (again)"),
- widget=forms.PasswordInput,
- strip=False,
- help_text=_("Enter the same password as before, for verification."),
- )
-
- def __init__(self, user, *args, **kwargs):
- self.user = user
- super().__init__(*args, **kwargs)
-
- def clean_password2(self):
- password1 = self.cleaned_data.get('password1')
- password2 = self.cleaned_data.get('password2')
- if password1 and password2:
- if password1 != password2:
- raise forms.ValidationError(
- self.error_messages['password_mismatch'],
- code='password_mismatch',
- )
- password_validation.validate_password(password2, self.user)
- return password2
-
- def save(self, commit=True):
- """Save the new password."""
- password = self.cleaned_data["password1"]
- self.user.set_password(password)
- if commit:
- self.user.save()
- return self.user
-
- @property
- def changed_data(self):
- data = super().changed_data
- for name in self.fields:
- if name not in data:
- return []
- return ['password']
|