382 lines
14 KiB
Python
382 lines
14 KiB
Python
from __future__ import unicode_literals
|
|
|
|
from collections import OrderedDict
|
|
|
|
from django import forms
|
|
from django.forms.util import flatatt
|
|
from django.template import loader
|
|
from django.utils.encoding import force_bytes
|
|
from django.utils.html import format_html, format_html_join
|
|
from django.utils.http import urlsafe_base64_encode
|
|
from django.utils.safestring import mark_safe
|
|
from django.utils.text import capfirst
|
|
from django.utils.translation import ugettext, ugettext_lazy as _
|
|
|
|
from django.contrib.auth import authenticate, get_user_model
|
|
from django.contrib.auth.models import User
|
|
from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, identify_hasher
|
|
from django.contrib.auth.tokens import default_token_generator
|
|
from django.contrib.sites.models import get_current_site
|
|
|
|
|
|
UNMASKED_DIGITS_TO_SHOW = 6
|
|
|
|
mask_password = lambda p: "%s%s" % (p[:UNMASKED_DIGITS_TO_SHOW], "*" * max(len(p) - UNMASKED_DIGITS_TO_SHOW, 0))
|
|
|
|
|
|
class ReadOnlyPasswordHashWidget(forms.Widget):
|
|
def render(self, name, value, attrs):
|
|
encoded = value
|
|
final_attrs = self.build_attrs(attrs)
|
|
|
|
if not encoded or encoded.startswith(UNUSABLE_PASSWORD_PREFIX):
|
|
summary = mark_safe("<strong>%s</strong>" % ugettext("No password set."))
|
|
else:
|
|
try:
|
|
hasher = identify_hasher(encoded)
|
|
except ValueError:
|
|
summary = mark_safe("<strong>%s</strong>" % ugettext(
|
|
"Invalid password format or unknown hashing algorithm."))
|
|
else:
|
|
summary = format_html_join('',
|
|
"<strong>{0}</strong>: {1} ",
|
|
((ugettext(key), value)
|
|
for key, value in hasher.safe_summary(encoded).items())
|
|
)
|
|
|
|
return format_html("<div{0}>{1}</div>", flatatt(final_attrs), summary)
|
|
|
|
|
|
class ReadOnlyPasswordHashField(forms.Field):
|
|
widget = ReadOnlyPasswordHashWidget
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs.setdefault("required", False)
|
|
super(ReadOnlyPasswordHashField, self).__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 UserCreationForm(forms.ModelForm):
|
|
"""
|
|
A form that creates a user, with no privileges, from the given username and
|
|
password.
|
|
"""
|
|
error_messages = {
|
|
'duplicate_username': _("A user with that username already exists."),
|
|
'password_mismatch': _("The two password fields didn't match."),
|
|
}
|
|
username = forms.RegexField(label=_("Username"), max_length=30,
|
|
regex=r'^[\w.@+-]+$',
|
|
help_text=_("Required. 30 characters or fewer. Letters, digits and "
|
|
"@/./+/-/_ only."),
|
|
error_messages={
|
|
'invalid': _("This value may contain only letters, numbers and "
|
|
"@/./+/-/_ characters.")})
|
|
password1 = forms.CharField(label=_("Password"),
|
|
widget=forms.PasswordInput)
|
|
password2 = forms.CharField(label=_("Password confirmation"),
|
|
widget=forms.PasswordInput,
|
|
help_text=_("Enter the same password as above, for verification."))
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = ("username",)
|
|
|
|
def clean_username(self):
|
|
# Since User.username is unique, this check is redundant,
|
|
# but it sets a nicer error message than the ORM. See #13147.
|
|
username = self.cleaned_data["username"]
|
|
try:
|
|
User._default_manager.get(username=username)
|
|
except User.DoesNotExist:
|
|
return username
|
|
raise forms.ValidationError(
|
|
self.error_messages['duplicate_username'],
|
|
code='duplicate_username',
|
|
)
|
|
|
|
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 save(self, commit=True):
|
|
user = super(UserCreationForm, self).save(commit=False)
|
|
user.set_password(self.cleaned_data["password1"])
|
|
if commit:
|
|
user.save()
|
|
return user
|
|
|
|
|
|
class UserChangeForm(forms.ModelForm):
|
|
username = forms.RegexField(
|
|
label=_("Username"), max_length=30, regex=r"^[\w.@+-]+$",
|
|
help_text=_("Required. 30 characters or fewer. Letters, digits and "
|
|
"@/./+/-/_ only."),
|
|
error_messages={
|
|
'invalid': _("This value may contain only letters, numbers and "
|
|
"@/./+/-/_ characters.")})
|
|
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=\"password/\">this form</a>."))
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = '__all__'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(UserChangeForm, self).__init__(*args, **kwargs)
|
|
f = self.fields.get('user_permissions', None)
|
|
if f is not None:
|
|
f.queryset = f.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["password"]
|
|
|
|
|
|
class AuthenticationForm(forms.Form):
|
|
"""
|
|
Base class for authenticating users. Extend this to get a form that accepts
|
|
username/password logins.
|
|
"""
|
|
username = forms.CharField(max_length=254)
|
|
password = forms.CharField(label=_("Password"), 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(AuthenticationForm, self).__init__(*args, **kwargs)
|
|
|
|
# Set the label for the "username" field.
|
|
UserModel = get_user_model()
|
|
self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
|
|
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 and password:
|
|
self.user_cache = authenticate(username=username,
|
|
password=password)
|
|
if self.user_cache is None:
|
|
raise forms.ValidationError(
|
|
self.error_messages['invalid_login'],
|
|
code='invalid_login',
|
|
params={'username': self.username_field.verbose_name},
|
|
)
|
|
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_id(self):
|
|
if self.user_cache:
|
|
return self.user_cache.id
|
|
return None
|
|
|
|
def get_user(self):
|
|
return self.user_cache
|
|
|
|
|
|
class PasswordResetForm(forms.Form):
|
|
email = forms.EmailField(label=_("Email"), max_length=254)
|
|
|
|
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):
|
|
"""
|
|
Generates a one-use only link for resetting password and sends to the
|
|
user.
|
|
"""
|
|
from django.core.mail import send_mail
|
|
UserModel = get_user_model()
|
|
email = self.cleaned_data["email"]
|
|
users = UserModel._default_manager.filter(email__iexact=email)
|
|
for user in users:
|
|
# Make sure that no email is sent to a user that actually has
|
|
# a password marked as unusable
|
|
if not user.has_usable_password():
|
|
continue
|
|
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
|
|
c = {
|
|
'email': user.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',
|
|
}
|
|
subject = loader.render_to_string(subject_template_name, c)
|
|
# Email subject *must not* contain newlines
|
|
subject = ''.join(subject.splitlines())
|
|
email = loader.render_to_string(email_template_name, c)
|
|
|
|
if html_email_template_name:
|
|
html_email = loader.render_to_string(html_email_template_name, c)
|
|
else:
|
|
html_email = None
|
|
send_mail(subject, email, from_email, [user.email], html_message=html_email)
|
|
|
|
|
|
class SetPasswordForm(forms.Form):
|
|
"""
|
|
A form that lets a user change set his/her 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)
|
|
new_password2 = forms.CharField(label=_("New password confirmation"),
|
|
widget=forms.PasswordInput)
|
|
|
|
def __init__(self, user, *args, **kwargs):
|
|
self.user = user
|
|
super(SetPasswordForm, self).__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',
|
|
)
|
|
return password2
|
|
|
|
def save(self, commit=True):
|
|
self.user.set_password(self.cleaned_data['new_password1'])
|
|
if commit:
|
|
self.user.save()
|
|
return self.user
|
|
|
|
|
|
class PasswordChangeForm(SetPasswordForm):
|
|
"""
|
|
A form that lets a user change his/her password by entering
|
|
their old password.
|
|
"""
|
|
error_messages = dict(SetPasswordForm.error_messages, **{
|
|
'password_incorrect': _("Your old password was entered incorrectly. "
|
|
"Please enter it again."),
|
|
})
|
|
old_password = forms.CharField(label=_("Old password"),
|
|
widget=forms.PasswordInput)
|
|
|
|
def clean_old_password(self):
|
|
"""
|
|
Validates 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
|
|
|
|
PasswordChangeForm.base_fields = OrderedDict(
|
|
(k, PasswordChangeForm.base_fields[k])
|
|
for k in ['old_password', 'new_password1', 'new_password2']
|
|
)
|
|
|
|
|
|
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."),
|
|
}
|
|
password1 = forms.CharField(label=_("Password"),
|
|
widget=forms.PasswordInput)
|
|
password2 = forms.CharField(label=_("Password (again)"),
|
|
widget=forms.PasswordInput)
|
|
|
|
def __init__(self, user, *args, **kwargs):
|
|
self.user = user
|
|
super(AdminPasswordChangeForm, self).__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',
|
|
)
|
|
return password2
|
|
|
|
def save(self, commit=True):
|
|
"""
|
|
Saves the new password.
|
|
"""
|
|
self.user.set_password(self.cleaned_data["password1"])
|
|
if commit:
|
|
self.user.save()
|
|
return self.user
|
|
|
|
def _get_changed_data(self):
|
|
data = super(AdminPasswordChangeForm, self).changed_data
|
|
for name in self.fields.keys():
|
|
if name not in data:
|
|
return []
|
|
return ['password']
|
|
changed_data = property(_get_changed_data)
|