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 HTMLString # For DisabledSelectField from wtforms.widgets import Select as SelectWidget # For DisabledSelectField from wtforms.widgets import html_params import dns.exception import dns.resolver 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_blacklist_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): 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 def register_email_server_validator(form, field): server_blacklist = app.config.get('EMAIL_SERVER_BLACKLIST', []) if not server_blacklist: return True validation_exception = StopValidation('Blacklisted email provider') email = field.data.strip() email_domain = email.split('@', 1)[-1] try: # Query domain MX records mx_records = list(dns.resolver.query(email_domain, 'MX')) except dns.exception.DNSException: app.logger.error('Unable to query MX records for email: %s - ignoring', email, exc_info=False) return True for mx_record in mx_records: try: # Query mailserver A records a_records = list(dns.resolver.query(mx_record.exchange)) for a_record in a_records: # Check for address in blacklist if a_record.address in server_blacklist: app.logger.warning('Rejected email %s due to blacklisted mailserver (%s, %s)', email, a_record.address, mx_record.exchange) raise validation_exception except dns.exception.DNSException: app.logger.warning('Failed to query A records for mailserver: %s (%s) - ignoring', mx_record.exchange, email, exc_info=False) 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_blacklist_validator, Unique(User, User.email, 'Email already in use by another account'), register_email_server_validator ]) 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') hide_comments = BooleanField('Hide comments by default') authorized_submit = SubmitField('Update') submit_settings = SubmitField('Update') # 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 ``