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
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_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
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 ``