"""Colander and Deform schemas."""
import re
from typing import List
from bag.text import strip_preparer, strip_lower_preparer
import colander as c
from deform.schema import CSRFSchema
import deform.widget as w
from kerno.kerno import Kerno
from kerno.web.pyramid import KRequest
from pluserable.strings import get_strings, _
[docs]def email_exists(node, val):
"""Colander validator that ensures a User exists with the email."""
request: KRequest = node.bindings["request"]
user = request.repo.get_user_by_email(val)
if not user:
raise c.Invalid(
node,
get_strings(request.registry).reset_password_email_must_exist.format(val),
)
[docs]def unique_email(node, val):
"""Colander validator that ensures the email does not exist."""
request: KRequest = node.bindings["request"]
other = request.repo.get_user_by_email(val)
if other:
raise c.Invalid(
node,
get_strings(request.registry).registration_email_exists.format(other.email),
)
[docs]def email_domain_allowed(node, val):
"""Colander validator that blocks configured email domains."""
kerno: Kerno = node.bindings["kerno"]
request: KRequest = node.bindings["request"]
blocked_domains: List[str] = kerno.settings["pluserable"].get(
"email_domains_blacklist", []
)
try:
left, domain = val.split("@", 1)
except ValueError:
raise c.Invalid(node, "An email address must contain an @ character")
if domain in blocked_domains:
raise c.Invalid(
node,
get_strings(request.registry).email_domain_blocked.format(domain),
)
[docs]def unique_username(node, val):
"""Colander validator that ensures the username does not exist."""
request: KRequest = node.bindings["request"]
user = request.repo.get_user_by_username(val)
if user is not None:
raise c.Invalid(
node, get_strings(request.registry).registration_username_exists
)
[docs]def unix_username(node, value):
"""Colander validator that ensures the username is alphanumeric."""
request: KRequest = node.bindings["request"]
if not ALPHANUM.match(value):
raise c.Invalid(node, get_strings(request.registry).unacceptable_characters)
ALPHANUM = re.compile(r"^[a-zA-Z0-9_.-]+$")
[docs]def username_does_not_contain_at(node, value):
"""Ensure the username does not contain an ``@`` character.
This is important because the system can be configured to accept
an email or a username in the same field at login time, so the
presence or absence of the @ tells us whether it is an email address.
This Colander validator is not being used by default. We are using the
``unix_username`` validator which does more. But we are keeping this
validator here in case someone wishes to use it instead of
``unix_username``.
"""
request: KRequest = node.bindings["request"]
if "@" in value:
raise c.Invalid(node, get_strings(request.registry).username_may_not_contain_at)
# Schema fragments
# ----------------
# These functions reduce duplication in the schemas defined below,
# while ensuring some constant values are consistent among those schemas.
[docs]def get_username_creation_node(
title=_("User name"),
description=_("Name with which you will log in"),
validator=None,
):
"""Return a reusable username node for Colander schemas."""
return c.SchemaNode(
c.String(),
title=title,
description=description,
preparer=strip_preparer,
validator=validator or c.All(c.Length(max=30), unix_username, unique_username),
)
[docs]def get_email_node(validator=None, description=None):
"""Return a reusable email address node for Colander schemas."""
return c.SchemaNode(
c.String(),
title=_("Email"),
description=description,
preparer=strip_lower_preparer,
validator=validator or c.All(c.Email(), unique_email, email_domain_allowed),
widget=w.TextInputWidget(
size=40,
maxlength=260,
type="email",
placeholder=_("joe@example.com"),
),
)
[docs]def get_checked_password_node(
description=_(
"Your password must be harder than a dictionary word or proper name!"
),
**kw,
):
return c.SchemaNode(
c.String(),
title=_("Password"),
validator=c.Length(min=4),
widget=w.CheckedPasswordWidget(),
description=description,
**kw,
)
# Schemas
# -------
[docs]class UsernameLoginSchema(CSRFSchema):
handle = c.SchemaNode(c.String(), title=_("User name"), preparer=strip_preparer)
password = c.SchemaNode(c.String(), widget=w.PasswordWidget())
[docs]class EmailLoginSchema(CSRFSchema):
"""For login, some apps just use email and have no username column."""
handle = get_email_node(validator=c.Email())
password = c.SchemaNode(c.String(), widget=w.PasswordWidget())
[docs]class UsernameRegisterSchema(CSRFSchema):
username = get_username_creation_node()
email = get_email_node()
password = get_checked_password_node()
[docs]class EmailRegisterSchema(CSRFSchema):
email = get_email_node()
password = get_checked_password_node()
[docs]class ForgotPasswordSchema(CSRFSchema):
email = get_email_node(
validator=c.All(c.Email(), email_exists),
description=_("The email address under which you have your account."),
)
[docs]class UsernameResetPasswordSchema(CSRFSchema):
username = c.SchemaNode(
c.String(),
title=_("User name"),
missing=c.null,
preparer=strip_preparer,
widget=w.TextInputWidget(template="readonly/textinput"),
)
password = get_checked_password_node()
[docs]class EmailResetPasswordSchema(CSRFSchema):
email = c.SchemaNode(
c.String(),
title=_("Email"),
missing=c.null,
preparer=strip_lower_preparer,
widget=w.TextInputWidget(template="readonly/textinput"),
)
password = get_checked_password_node()
[docs]class UsernameProfileSchema(CSRFSchema):
username = c.SchemaNode(
c.String(),
widget=w.TextInputWidget(template="readonly/textinput"),
preparer=strip_preparer,
missing=c.null,
)
email = get_email_node(description=None, validator=c.Email())
password = get_checked_password_node(missing=c.null)
[docs]class EmailProfileSchema(CSRFSchema):
email = get_email_node(description=None, validator=c.Email())
password = get_checked_password_node(missing=c.null)