# -*- coding: utf-8 -*- """ flask_security.forms ~~~~~~~~~~~~~~~~~~~~ Flask-Security forms module :copyright: (c) 2012 by Matt Wright. :copyright: (c) 2017 by CERN. :license: MIT, see LICENSE for more details. """ import inspect from flask import Markup, current_app, flash, request from flask_login import current_user from flask_wtf import FlaskForm as BaseForm from speaklater import make_lazy_gettext from wtforms import BooleanField, Field, HiddenField, PasswordField, \ StringField, SubmitField, ValidationError, validators from .confirmable import requires_confirmation from .utils import _, _datastore, config_value, get_message, \ localize_callback, url_for_security, validate_redirect_url, \ verify_and_update_password lazy_gettext = make_lazy_gettext(lambda: localize_callback) _default_field_labels = { 'email': _('Email Address'), 'password': _('Password'), 'remember_me': _('Remember Me'), 'login': _('Login'), 'register': _('Register'), 'send_confirmation': _('Resend Confirmation Instructions'), 'recover_password': _('Recover Password'), 'reset_password': _('Reset Password'), 'retype_password': _('Retype Password'), 'new_password': _('New Password'), 'change_password': _('Change Password'), 'send_login_link': _('Send Login Link') } class ValidatorMixin(object): def __call__(self, form, field): if self.message and self.message.isupper(): self.message = get_message(self.message)[0] return super(ValidatorMixin, self).__call__(form, field) class EqualTo(ValidatorMixin, validators.EqualTo): pass class Required(ValidatorMixin, validators.DataRequired): pass class Email(ValidatorMixin, validators.Email): pass class Length(ValidatorMixin, validators.Length): pass email_required = Required(message='EMAIL_NOT_PROVIDED') email_validator = Email(message='INVALID_EMAIL_ADDRESS') password_required = Required(message='PASSWORD_NOT_PROVIDED') password_length = Length(min=6, max=128, message='PASSWORD_INVALID_LENGTH') def get_form_field_label(key): return lazy_gettext(_default_field_labels.get(key, '')) def unique_user_email(form, field): if _datastore.get_user(field.data) is not None: msg = get_message('EMAIL_ALREADY_ASSOCIATED', email=field.data)[0] raise ValidationError(msg) def valid_user_email(form, field): form.user = _datastore.get_user(field.data) if form.user is None: raise ValidationError(get_message('USER_DOES_NOT_EXIST')[0]) class Form(BaseForm): def __init__(self, *args, **kwargs): if current_app.testing: self.TIME_LIMIT = None super(Form, self).__init__(*args, **kwargs) class EmailFormMixin(): email = StringField( get_form_field_label('email'), validators=[email_required, email_validator]) class UserEmailFormMixin(): user = None email = StringField( get_form_field_label('email'), validators=[email_required, email_validator, valid_user_email]) class UniqueEmailFormMixin(): email = StringField( get_form_field_label('email'), validators=[email_required, email_validator, unique_user_email]) class PasswordFormMixin(): password = PasswordField( get_form_field_label('password'), validators=[password_required]) class NewPasswordFormMixin(): password = PasswordField( get_form_field_label('password'), validators=[password_required, password_length]) class PasswordConfirmFormMixin(): password_confirm = PasswordField( get_form_field_label('retype_password'), validators=[EqualTo('password', message='RETYPE_PASSWORD_MISMATCH'), password_required]) class NextFormMixin(): next = HiddenField() def validate_next(self, field): if field.data and not validate_redirect_url(field.data): field.data = '' flash(*get_message('INVALID_REDIRECT')) raise ValidationError(get_message('INVALID_REDIRECT')[0]) class RegisterFormMixin(): submit = SubmitField(get_form_field_label('register')) def to_dict(form): def is_field_and_user_attr(member): return isinstance(member, Field) and \ hasattr(_datastore.user_model, member.name) fields = inspect.getmembers(form, is_field_and_user_attr) return dict((key, value.data) for key, value in fields) class SendConfirmationForm(Form, UserEmailFormMixin): """The default forgot password form""" submit = SubmitField(get_form_field_label('send_confirmation')) def __init__(self, *args, **kwargs): super(SendConfirmationForm, self).__init__(*args, **kwargs) if request.method == 'GET': self.email.data = request.args.get('email', None) def validate(self): if not super(SendConfirmationForm, self).validate(): return False if self.user.confirmed_at is not None: self.email.errors.append(get_message('ALREADY_CONFIRMED')[0]) return False return True class ForgotPasswordForm(Form, UserEmailFormMixin): """The default forgot password form""" submit = SubmitField(get_form_field_label('recover_password')) def validate(self): if not super(ForgotPasswordForm, self).validate(): return False if requires_confirmation(self.user): self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0]) return False return True class PasswordlessLoginForm(Form, UserEmailFormMixin): """The passwordless login form""" submit = SubmitField(get_form_field_label('send_login_link')) def __init__(self, *args, **kwargs): super(PasswordlessLoginForm, self).__init__(*args, **kwargs) def validate(self): if not super(PasswordlessLoginForm, self).validate(): return False if not self.user.is_active: self.email.errors.append(get_message('DISABLED_ACCOUNT')[0]) return False return True class LoginForm(Form, NextFormMixin): """The default login form""" email = StringField(get_form_field_label('email'), validators=[Required(message='EMAIL_NOT_PROVIDED')]) password = PasswordField(get_form_field_label('password'), validators=[password_required]) remember = BooleanField(get_form_field_label('remember_me')) submit = SubmitField(get_form_field_label('login')) def __init__(self, *args, **kwargs): super(LoginForm, self).__init__(*args, **kwargs) if not self.next.data: self.next.data = request.args.get('next', '') self.remember.default = config_value('DEFAULT_REMEMBER_ME') if current_app.extensions['security'].recoverable and \ not self.password.description: html = Markup(u'{message}'.format( url=url_for_security("forgot_password"), message=get_message("FORGOT_PASSWORD")[0], )) self.password.description = html def validate(self): if not super(LoginForm, self).validate(): return False self.user = _datastore.get_user(self.email.data) if self.user is None: self.email.errors.append(get_message('USER_DOES_NOT_EXIST')[0]) return False if not self.user.password: self.password.errors.append(get_message('PASSWORD_NOT_SET')[0]) return False if not verify_and_update_password(self.password.data, self.user): self.password.errors.append(get_message('INVALID_PASSWORD')[0]) return False if requires_confirmation(self.user): self.email.errors.append(get_message('CONFIRMATION_REQUIRED')[0]) return False if not self.user.is_active: self.email.errors.append(get_message('DISABLED_ACCOUNT')[0]) return False return True class ConfirmRegisterForm(Form, RegisterFormMixin, UniqueEmailFormMixin, NewPasswordFormMixin): pass class RegisterForm(ConfirmRegisterForm, PasswordConfirmFormMixin, NextFormMixin): def __init__(self, *args, **kwargs): super(RegisterForm, self).__init__(*args, **kwargs) if not self.next.data: self.next.data = request.args.get('next', '') class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin): """The default reset password form""" submit = SubmitField(get_form_field_label('reset_password')) class ChangePasswordForm(Form, PasswordFormMixin): """The default change password form""" new_password = PasswordField( get_form_field_label('new_password'), validators=[password_required, password_length]) new_password_confirm = PasswordField( get_form_field_label('retype_password'), validators=[EqualTo('new_password', message='RETYPE_PASSWORD_MISMATCH'), password_required]) submit = SubmitField(get_form_field_label('change_password')) def validate(self): if not super(ChangePasswordForm, self).validate(): return False if not verify_and_update_password(self.password.data, current_user): self.password.errors.append(get_message('INVALID_PASSWORD')[0]) return False if self.password.data == self.new_password.data: self.password.errors.append(get_message('PASSWORD_IS_THE_SAME')[0]) return False return True