import functools import os import re import flask from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileRequired from flask_wtf.recaptcha import RecaptchaField from flask_wtf.recaptcha.validators import Recaptcha as RecaptchaValidator from wtforms import (BooleanField, HiddenField, PasswordField, SelectField, StringField, SubmitField, TextAreaField) from wtforms.validators import (DataRequired, Email, EqualTo, Length, Optional, Regexp, StopValidation, ValidationError) from wtforms.widgets import Select as SelectWidget # For DisabledSelectField from wtforms.widgets import HTMLString, html_params # For DisabledSelectField from nyaa import bencode, models, utils from nyaa.extensions import config from nyaa.models import User app = flask.current_app class Unique(object): """ validator that checks field uniqueness """ def __init__(self, model, field, message=None): self.model = model self.field = field if not message: message = 'This element already exists' self.message = message def __call__(self, form, field): check = self.model.query.filter(self.field == field.data).first() if check: raise ValidationError(self.message) def stop_on_validation_error(f): ''' A decorator which will turn raised ValidationErrors into StopValidations ''' @functools.wraps(f) def decorator(*args, **kwargs): try: return f(*args, **kwargs) except ValidationError as e: # Replace the error with a StopValidation to stop the validation chain raise StopValidation(*e.args) from e return decorator def recaptcha_validator_shim(form, field): if app.config['USE_RECAPTCHA']: return RecaptchaValidator()(form, field) else: # Always pass validating the recaptcha field if disabled return True def upload_recaptcha_validator_shim(form, field): ''' Selectively does a recaptcha validation ''' if app.config['USE_RECAPTCHA']: # Recaptcha anonymous and new users if not flask.g.user or flask.g.user.age < app.config['ACCOUNT_RECAPTCHA_AGE']: return RecaptchaValidator()(form, field) else: # Always pass validating the recaptcha field if disabled return True def register_email_validator(form, field): email_blacklist = app.config.get('EMAIL_BLACKLIST', []) email = field.data.strip() validation_exception = StopValidation('Blacklisted email provider') for item in email_blacklist: if isinstance(item, re._pattern_type): if item.search(email): raise validation_exception elif isinstance(item, str): if item in email.lower(): raise validation_exception else: raise Exception('Unexpected email validator type {!r} ({!r})'.format(type(item), item)) return True _username_validator = Regexp( r'^[a-zA-Z0-9_\-]+$', message='Your username must only consist of alphanumerics and _- (a-zA-Z0-9_-)') class LoginForm(FlaskForm): username = StringField('Username or email address', [DataRequired()]) password = PasswordField('Password', [DataRequired()]) class PasswordResetRequestForm(FlaskForm): email = StringField('Email address', [ Email(), DataRequired(), Length(min=5, max=128) ]) recaptcha = RecaptchaField(validators=[recaptcha_validator_shim]) class PasswordResetForm(FlaskForm): password = PasswordField('Password', [ DataRequired(), EqualTo('password_confirm', message='Passwords must match'), Length(min=6, max=1024, message='Password must be at least %(min)d characters long.') ]) password_confirm = PasswordField('Password (confirm)') class RegisterForm(FlaskForm): username = StringField('Username', [ DataRequired(), Length(min=3, max=32), stop_on_validation_error(_username_validator), Unique(User, User.username, 'Username not available') ]) email = StringField('Email address', [ Email(), DataRequired(), Length(min=5, max=128), register_email_validator, Unique(User, User.email, 'Email already in use by another account') ]) password = PasswordField('Password', [ DataRequired(), EqualTo('password_confirm', message='Passwords must match'), Length(min=6, max=1024, message='Password must be at least %(min)d characters long.') ]) password_confirm = PasswordField('Password (confirm)') if config['USE_RECAPTCHA']: recaptcha = RecaptchaField() class ProfileForm(FlaskForm): email = StringField('New Email Address', [ Email(), Optional(), Length(min=5, max=128), Unique(User, User.email, 'This email address has been taken') ]) current_password = PasswordField('Current Password', [DataRequired()]) new_password = PasswordField('New Password', [ Optional(), EqualTo('password_confirm', message='Two passwords must match'), Length(min=6, max=1024, message='Password must be at least %(min)d characters long.') ]) password_confirm = PasswordField('Repeat New Password') # Classes for a SelectField that can be set to disable options (id, name, disabled) # TODO: Move to another file for cleaner look class DisabledSelectWidget(SelectWidget): def __call__(self, field, **kwargs): kwargs.setdefault('id', field.id) if self.multiple: kwargs['multiple'] = True html = ['') return HTMLString(''.join(html)) class DisabledSelectField(SelectField): widget = DisabledSelectWidget() def iter_choices(self): for choice_tuple in self.choices: value, label = choice_tuple[:2] disabled = len(choice_tuple) == 3 and choice_tuple[2] or False yield (value, label, self.coerce(value) == self.data, disabled) def pre_validate(self, form): for v in self.choices: if self.data == v[0]: break else: raise ValueError(self.gettext('Not a valid choice')) class CommentForm(FlaskForm): comment = TextAreaField('Make a comment', [ Length(min=3, max=2048, message='Comment must be at least %(min)d characters ' 'long and %(max)d at most.'), DataRequired(message='Comment must not be empty.') ]) recaptcha = RecaptchaField(validators=[upload_recaptcha_validator_shim]) class InlineButtonWidget(object): """ Render a basic ``