Warning
This document is for an in-development version of Galaxy. You can alternatively view this page in the latest release if it exists or view the top of the latest release's documentation.
Source code for galaxy.util.user_input
"""
Rules for user account fields that depend on nothing but the value.
galaxy.schema validates API payloads with these and the managers apply them for
every other caller, so a field has one rule however its value arrives. The
validate_* functions return a user-facing message, or "" for a valid value, and
never echo the input. Rules that need the database or configuration, such as
uniqueness, live in galaxy.security.validate_user_input.
"""
import re
import unicodedata
# Email validity parameters
#
# Many words (and regexes) have been written about validating email addresses and there is no perfect answer on how it
# should be done. We choose to use the HTML5 spec (and corresponding regex) that engages in a "willful violation" of RFC
# 5322 to provide a reasonably good validation. Additionally, we allow Unicode characters in both the user and domain
# parts of the email by using re's '\w' character. Note that \w includes "word" characters but appears to exclude emoji
# characters, which should in fact be valid.
#
# https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
VALID_EMAIL_RE = re.compile(r"^[\w.!#$%&'*+\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$")
EMAIL_MAX_LEN = 255
# Display name validity parameters
#
# Display names are free-form and may be in any script, so the rule rejects
# what cannot be seen rather than listing what may be used. Bidirectional
# formatting characters are the main reason the check exists: U+202E and
# friends let a name render as text entirely unrelated to what is stored, which
# makes a display name a viable impersonation vector wherever it is shown. The
# other control, format, line and paragraph separator, private-use, surrogate
# and unassigned code points go with them, as do the letters and marks that
# render as blank space.
#
# The zero-width joiner and non-joiner are the exception. Persian, the Indic
# scripts and emoji sequences need them, so they are allowed where they can do
# that job: between two visible characters.
DISPLAY_NAME_MAX_LEN = 255
DISPLAY_NAME_REJECTED_CATEGORIES = frozenset({"Cc", "Cf", "Cn", "Co", "Cs", "Zl", "Zp"})
DISPLAY_NAME_BLANK_CHARACTERS = frozenset("\u034f\u115f\u1160\u17b4\u17b5\u2800\u3164\uffa0")
DISPLAY_NAME_JOINERS = frozenset("\u200c\u200d")
# Public name validity parameters
PUBLICNAME_MAX_LEN = 255
VALID_PUBLICNAME_RE = re.compile(r"^[a-z0-9._\-]+$")
VALID_PUBLICNAME_SUB = re.compile(r"[^a-z0-9._\-]")
FILL_CHAR = "-"
# Password validity parameters
PASSWORD_MIN_LEN = 6
def canonicalize_email(email: str | None) -> str:
"""Return the form of an email address that is validated and stored.
Surrounding whitespace is stripped; None becomes "", which validate_email_str reports as missing.
"""
return (email or "").strip()
[docs]
def is_valid_email_str(email: str | None) -> bool:
"""Validates a string containing an email address and returns a boolean result."""
return validate_email_str(email) == ""
[docs]
def validate_email_str(email: str | None) -> str:
"""Validates a string containing an email address."""
if not email:
return "No email address was provided."
if not (VALID_EMAIL_RE.match(email)):
return "The format of the email address is not correct."
elif len(email) > EMAIL_MAX_LEN:
return f"Email address cannot be more than {EMAIL_MAX_LEN} characters in length."
return ""
[docs]
def validate_publicname_str(publicname: str | None) -> str:
"""Validates a string containing a public username."""
if not publicname:
return "Public name cannot be empty"
if len(publicname) > PUBLICNAME_MAX_LEN:
return f"Public name cannot be more than {PUBLICNAME_MAX_LEN} characters in length."
if not (VALID_PUBLICNAME_RE.match(publicname)):
return "Public name must contain only lower-case letters, numbers, '.', '_' and '-'."
return ""
[docs]
def transform_publicname(publicname: str | None) -> str:
"""
Transform publicname to respect the minimum and maximum string length, and
the allowed characters.
FILL_CHAR is used to extend or replace characters.
"""
# TODO: Enhance to allow generation of semi-random publicnnames e.g., when valid but taken
if not publicname:
raise ValueError("Public name cannot be empty")
publicname = publicname.lower()
publicname = re.sub(VALID_PUBLICNAME_SUB, FILL_CHAR, publicname)
publicname = publicname[:PUBLICNAME_MAX_LEN]
return publicname
[docs]
def validate_password_str(password: str | None) -> str:
if not password or len(password) < PASSWORD_MIN_LEN:
return f"Use a password of at least {PASSWORD_MIN_LEN} characters."
return ""
def canonicalize_display_name(display_name: str | None) -> str | None:
"""Return the form of a display name that is validated and stored.
The name is NFC-normalized so that composed and decomposed spellings are
stored (and measured) alike, and surrounding whitespace is stripped rather
than rejected. A name that is empty once stripped becomes None, which
clears the field - an invisible difference is a poor reason to fail a save.
"""
return unicodedata.normalize("NFC", display_name or "").strip() or None
def validate_display_name_str(display_name: str | None) -> str:
"""Validates a string containing a user's display name.
Callers are expected to have passed the value through
canonicalize_display_name; an empty display name means "unset" and is
accepted here so that clearing the field is not an error.
"""
if not display_name:
return ""
if len(display_name) > DISPLAY_NAME_MAX_LEN:
return f"Display name cannot be more than {DISPLAY_NAME_MAX_LEN} characters in length."
last = len(display_name) - 1
for i, char in enumerate(display_name):
if char in DISPLAY_NAME_JOINERS:
if not (0 < i < last and _is_joinable(display_name[i - 1]) and _is_joinable(display_name[i + 1])):
return "Display name cannot contain control, formatting or invisible characters."
elif unicodedata.category(char) in DISPLAY_NAME_REJECTED_CATEGORIES or char in DISPLAY_NAME_BLANK_CHARACTERS:
return "Display name cannot contain control, formatting or invisible characters."
if not any(_is_visible(char) for char in display_name):
return "Display name must contain at least one visible character."
return ""
def _is_visible(char: str) -> bool:
# Letters, numbers, punctuation and symbols; not marks, separators or other.
return unicodedata.category(char)[0] in "LNPS" and char not in DISPLAY_NAME_BLANK_CHARACTERS
def _is_joinable(char: str) -> bool:
# A joiner may follow or precede a combining mark (a virama, an emoji
# variation selector) but not a space, a format character or another joiner.
return unicodedata.category(char)[0] not in "CZ" and char not in DISPLAY_NAME_BLANK_CHARACTERS