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 markupsafe import Markup
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 Markup(''.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 ``