Files
edx-platform/common/djangoapps/student/forms.py
uzairr 8ffac2061d Verify 'Full Name' field does not allow HTML in Signup form
'Full Name' field in the signup form is allowing HTML as an input
which makes spoofing easily.To avoid it, validation is added
that will ensure 'Full Name' field does not allow HTML.

LEARNER-3385
2017-12-08 19:18:18 +05:00

342 lines
13 KiB
Python

"""
Utility functions for validating forms
"""
import re
from importlib import import_module
from django import forms
from django.conf import settings
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX
from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.template import loader
from django.utils.http import int_to_base36
from django.utils.translation import ugettext_lazy as _
from django.core.validators import RegexValidator, slug_re
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_api import accounts as accounts_settings
from student.models import CourseEnrollmentAllowed
from util.password_policy_validators import validate_password_strength
class PasswordResetFormNoActive(PasswordResetForm):
error_messages = {
'unknown': _("That e-mail address doesn't have an associated "
"user account. Are you sure you've registered?"),
'unusable': _("The user account associated with this e-mail "
"address cannot reset the password."),
}
def clean_email(self):
"""
This is a literal copy from Django 1.4.5's django.contrib.auth.forms.PasswordResetForm
Except removing the requirement of active users
Validates that a user exists with the given email address.
"""
email = self.cleaned_data["email"]
#The line below contains the only change, removing is_active=True
self.users_cache = User.objects.filter(email__iexact=email)
if not len(self.users_cache):
raise forms.ValidationError(self.error_messages['unknown'])
if any((user.password.startswith(UNUSABLE_PASSWORD_PREFIX))
for user in self.users_cache):
raise forms.ValidationError(self.error_messages['unusable'])
return email
def save(
self,
subject_template_name='emails/password_reset_subject.txt',
email_template_name='registration/password_reset_email.html',
use_https=False,
token_generator=default_token_generator,
from_email=configuration_helpers.get_value('email_from_address', settings.DEFAULT_FROM_EMAIL),
request=None
):
"""
Generates a one-use only link for resetting password and sends to the
user.
"""
# This import is here because we are copying and modifying the .save from Django 1.4.5's
# django.contrib.auth.forms.PasswordResetForm directly, which has this import in this place.
from django.core.mail import send_mail
for user in self.users_cache:
site_name = configuration_helpers.get_value(
'SITE_NAME',
settings.SITE_NAME
)
context = {
'email': user.email,
'site_name': site_name,
'uid': int_to_base36(user.id),
'user': user,
'token': token_generator.make_token(user),
'protocol': 'https' if use_https else 'http',
'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME)
}
subject = loader.render_to_string(subject_template_name, context)
# Email subject *must not* contain newlines
subject = subject.replace('\n', '')
email = loader.render_to_string(email_template_name, context)
send_mail(subject, email, from_email, [user.email])
class TrueCheckbox(widgets.CheckboxInput):
"""
A checkbox widget that only accepts "true" (case-insensitive) as true.
"""
def value_from_datadict(self, data, files, name):
value = data.get(name, '')
return value.lower() == 'true'
class TrueField(forms.BooleanField):
"""
A boolean field that only accepts "true" (case-insensitive) as true
"""
widget = TrueCheckbox
def validate_username(username):
"""
Verifies a username is valid, raises a ValidationError otherwise.
Args:
username (unicode): The username to validate.
This function is configurable with `ENABLE_UNICODE_USERNAME` feature.
"""
username_re = slug_re
flags = None
message = accounts_settings.USERNAME_INVALID_CHARS_ASCII
if settings.FEATURES.get("ENABLE_UNICODE_USERNAME"):
username_re = r"^{regex}$".format(regex=settings.USERNAME_REGEX_PARTIAL)
flags = re.UNICODE
message = accounts_settings.USERNAME_INVALID_CHARS_UNICODE
validator = RegexValidator(
regex=username_re,
flags=flags,
message=message,
code='invalid',
)
validator(username)
def validate_name(name):
"""
Verifies a Full_Name is valid, raises a ValidationError otherwise.
Args:
name (unicode): The name to validate.
"""
if accounts_settings.api.contains_html(name):
raise forms.ValidationError(_('Full Name cannot contain the following characters: < >'))
class UsernameField(forms.CharField):
"""
A CharField that validates usernames based on the `ENABLE_UNICODE_USERNAME` feature.
"""
default_validators = [validate_username]
def __init__(self, *args, **kwargs):
super(UsernameField, self).__init__(
min_length=accounts_settings.USERNAME_MIN_LENGTH,
max_length=accounts_settings.USERNAME_MAX_LENGTH,
error_messages={
"required": accounts_settings.USERNAME_BAD_LENGTH_MSG,
"min_length": accounts_settings.USERNAME_BAD_LENGTH_MSG,
"max_length": accounts_settings.USERNAME_BAD_LENGTH_MSG,
}
)
def clean(self, value):
"""
Strips the spaces from the username.
Similar to what `django.forms.SlugField` does.
"""
value = self.to_python(value).strip()
return super(UsernameField, self).clean(value)
class AccountCreationForm(forms.Form):
"""
A form to for account creation data. It is currently only used for
validation, not rendering.
"""
_EMAIL_INVALID_MSG = _("A properly formatted e-mail is required")
_PASSWORD_INVALID_MSG = _("A valid password is required")
_NAME_TOO_SHORT_MSG = _("Your legal name must be a minimum of two characters long")
# TODO: Resolve repetition
username = UsernameField()
email = forms.EmailField(
max_length=accounts_settings.EMAIL_MAX_LENGTH,
min_length=accounts_settings.EMAIL_MIN_LENGTH,
error_messages={
"required": _EMAIL_INVALID_MSG,
"invalid": _EMAIL_INVALID_MSG,
"max_length": _("Email cannot be more than %(limit_value)s characters long"),
}
)
password = forms.CharField(
min_length=accounts_settings.PASSWORD_MIN_LENGTH,
error_messages={
"required": _PASSWORD_INVALID_MSG,
"min_length": _PASSWORD_INVALID_MSG,
}
)
name = forms.CharField(
min_length=accounts_settings.NAME_MIN_LENGTH,
error_messages={
"required": _NAME_TOO_SHORT_MSG,
"min_length": _NAME_TOO_SHORT_MSG,
},
validators=[validate_name]
)
def __init__(
self,
data=None,
extra_fields=None,
extended_profile_fields=None,
enforce_username_neq_password=False,
enforce_password_policy=False,
tos_required=True
):
super(AccountCreationForm, self).__init__(data)
extra_fields = extra_fields or {}
self.extended_profile_fields = extended_profile_fields or {}
self.enforce_username_neq_password = enforce_username_neq_password
self.enforce_password_policy = enforce_password_policy
if tos_required:
self.fields["terms_of_service"] = TrueField(
error_messages={"required": _("You must accept the terms of service.")}
)
# TODO: These messages don't say anything about minimum length
error_message_dict = {
"level_of_education": _("A level of education is required"),
"gender": _("Your gender is required"),
"year_of_birth": _("Your year of birth is required"),
"mailing_address": _("Your mailing address is required"),
"goals": _("A description of your goals is required"),
"city": _("A city is required"),
"country": _("A country is required")
}
for field_name, field_value in extra_fields.items():
if field_name not in self.fields:
if field_name == "honor_code":
if field_value == "required":
self.fields[field_name] = TrueField(
error_messages={
"required": _("To enroll, you must follow the honor code.")
}
)
else:
required = field_value == "required"
min_length = 1 if field_name in ("gender", "level_of_education") else 2
error_message = error_message_dict.get(
field_name,
_("You are missing one or more required fields")
)
self.fields[field_name] = forms.CharField(
required=required,
min_length=min_length,
error_messages={
"required": error_message,
"min_length": error_message,
}
)
for field in self.extended_profile_fields:
if field not in self.fields:
self.fields[field] = forms.CharField(required=False)
def clean_password(self):
"""Enforce password policies (if applicable)"""
password = self.cleaned_data["password"]
if (
self.enforce_username_neq_password and
"username" in self.cleaned_data and
self.cleaned_data["username"] == password
):
raise ValidationError(_("Username and password fields cannot match"))
if self.enforce_password_policy:
try:
validate_password_strength(password)
except ValidationError, err:
raise ValidationError(_("Password: ") + "; ".join(err.messages))
return password
def clean_email(self):
""" Enforce email restrictions (if applicable) """
email = self.cleaned_data["email"]
if settings.REGISTRATION_EMAIL_PATTERNS_ALLOWED is not None:
# This Open edX instance has restrictions on what email addresses are allowed.
allowed_patterns = settings.REGISTRATION_EMAIL_PATTERNS_ALLOWED
# We append a '$' to the regexs to prevent the common mistake of using a
# pattern like '.*@edx\\.org' which would match 'bob@edx.org.badguy.com'
if not any(re.match(pattern + "$", email) for pattern in allowed_patterns):
# This email is not on the whitelist of allowed emails. Check if
# they may have been manually invited by an instructor and if not,
# reject the registration.
if not CourseEnrollmentAllowed.objects.filter(email=email).exists():
raise ValidationError(_("Unauthorized email address."))
if User.objects.filter(email__iexact=email).exists():
raise ValidationError(
_(
"It looks like {email} belongs to an existing account. Try again with a different email address."
).format(email=email)
)
return email
def clean_year_of_birth(self):
"""
Parse year_of_birth to an integer, but just use None instead of raising
an error if it is malformed
"""
try:
year_str = self.cleaned_data["year_of_birth"]
return int(year_str) if year_str is not None else None
except ValueError:
return None
@property
def cleaned_extended_profile(self):
"""
Return a dictionary containing the extended_profile_fields and values
"""
return {
key: value
for key, value in self.cleaned_data.items()
if key in self.extended_profile_fields and value is not None
}
def get_registration_extension_form(*args, **kwargs):
"""
Convenience function for getting the custom form set in settings.REGISTRATION_EXTENSION_FORM.
An example form app for this can be found at http://github.com/open-craft/custom-form-app
"""
if not settings.FEATURES.get("ENABLE_COMBINED_LOGIN_REGISTRATION"):
return None
if not getattr(settings, 'REGISTRATION_EXTENSION_FORM', None):
return None
module, klass = settings.REGISTRATION_EXTENSION_FORM.rsplit('.', 1)
module = import_module(module)
return getattr(module, klass)(*args, **kwargs)