blob: ffb3f4625093a739333ba93213c7f00d238c249d [file] [log] [blame]
# Copyright 2008 the Melange authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generic cleaning methods."""
import HTMLParser
import re
from google.appengine.api import users
from django import forms
from django.core import validators
from django.utils import translation
from django.utils import safestring
import html5lib
from html5lib import sanitizer
from html5lib import html5parser
from melange.logic import user as user_logic
from melange.models import user as user_model
from soc.logic import validate
DEF_INVALID_EMAIL_ADDRESS = translation.ugettext(
'"%s" is not a valid email address.')
DEF_INVALID_LINK_ID = translation.ugettext(
'"%s" is not a properly-formed username.')
DEF_VALID_SHIPPING_CHARS = re.compile('^[A-Za-z0-9\s-]+$')
DEF_LINK_ID_IN_USE = translation.ugettext(
'This username is already in use, please specify another one')
DEF_NO_RIGHTS_FOR_ACL = translation.ugettext(
'You do not have the required rights for that ACL.')
DEF_ORGANZIATION_NOT_ACTIVE = translation.ugettext(
"This organization is not active or doesn't exist.")
DEF_NO_SUCH_DOCUMENT = translation.ugettext(
'There is no such document with that username under this entity.')
DEF_MUST_BE_ABOVE_AGE_LIMIT = translation.ugettext(
'To sign up as a student for this program, you '
'must be at least %d years of age, as of %s.')
DEF_MUST_BE_ABOVE_LIMIT = translation.ugettext(
'Must be at least %d characters, it has %d characters.')
DEF_MUST_BE_UNDER_LIMIT = translation.ugettext(
'Must be under %d characters, it has %d characters.')
DEF_2_LETTER_STATE = translation.ugettext(
'State should be 2-letter field since country is "%s".')
DEF_INVALID_SHIPPING_CHARS = translation.ugettext(
'Invalid characters, only A-z, 0-9, - and whitespace are allowed. '
'See also <a href="http://code.google.com/p/soc/issues/detail?id=903">'
'Issue 903</a>, in particular <a href="'
'http://code.google.com/p/soc/issues/detail?id=903#c16">comment 16</a>. '
'Please <em>do not</em> create a new issue about this.')
DEF_ROLE_TARGET_COUNTRY = 'United States'
DEF_ROLE_COUNTRY_PAIRS = [('res_country', 'res_state'),
('ship_country', 'ship_state')]
USER_DOES_NOT_EXIST_ERROR_MSG = 'User %s does not exist.'
# The Django documentation is unclear on why ValidationErrors carry a
# "code" and what that code is supposed to signify, but for some reason
# this seems to be necessary.
_INVALID_CODE = 'invalid'
def check_field_is_empty(field_name):
"""Returns decorator that bypasses cleaning for empty fields.
"""
def decorator(fun):
"""Decorator that checks if a field is empty if so doesn't do the cleaning.
Note Django will capture errors concerning required fields that are empty.
"""
from functools import wraps
@wraps(fun)
def wrapper(self):
"""Decorator wrapper method.
"""
field_content = self.cleaned_data.get(field_name)
if not field_content:
# field has no content so bail out
return u''
else:
# field has contents
return fun(self)
return wrapper
return decorator
def clean_empty_field(field_name):
"""Incorporates the check_field_is_empty as regular cleaner.
"""
@check_field_is_empty(field_name)
def wrapper(self):
"""Decorator wrapper method.
"""
return self.cleaned_data.get(field_name)
return wrapper
def cleanEmail(email):
"""Validates that a string is a properly formed email address.
Args:
email: Any string.
Raises:
forms.ValidationError: The string is not a valid email address.
"""
try:
validators.validate_email(email)
except forms.ValidationError as e:
# NOTE(nathaniel): The message that Django supplies in its raised
# ValidationError does not include the string that generated it,
# so for user-friendliness we substitute our own message that
# does include the input string.
message = translation.ugettext(DEF_INVALID_EMAIL_ADDRESS % email)
raise forms.ValidationError(message, code=e.code)
# TODO(nathaniel): Find some terms with which to document this. It's
# like a mixin... function... method... thing?
def clean_email(field_name):
"""Checks if the field_name value is in an email format.
"""
@check_field_is_empty(field_name)
def wrapper(self):
"""Decorator wrapper method.
"""
# convert to lowercase for user comfort
email = self.cleaned_data.get(field_name)
cleanEmail(email)
return email
return wrapper
def cleanLinkID(link_id):
"""Validates that a string fits the form required of a link ID.
Args:
link_id: Any string.
Raises:
forms.ValidationError: The string does not fit the form required
of a link ID.
"""
if not validate.isLinkIdFormatValid(link_id):
raise forms.ValidationError(
DEF_INVALID_LINK_ID % link_id, code=_INVALID_CODE)
# TODO(nathaniel): Document this too in some form or another.
def clean_link_id(field_name):
"""Checks if the field_name value is in a valid link ID format.
"""
@check_field_is_empty(field_name)
def wrapper(self):
"""Decorator wrapper method.
"""
# convert to lowercase for user comfort
link_id = self.cleaned_data.get(field_name).lower()
cleanLinkID(link_id)
return link_id
return wrapper
def clean_existing_user(field_name):
"""Check if the field_name field is a valid user.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method."""
user_id = clean_link_id(field_name)(self)
user = user_model.User.get_by_id(user_id)
if not user:
# user does not exist
raise forms.ValidationError(
USER_DOES_NOT_EXIST_ERROR_MSG % user_id)
return user
return wrapped
def clean_user_is_current(field_name, as_user=True):
"""Check if the field_name value is a valid link_id and resembles the
current user.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method."""
user_id = clean_link_id(field_name)(self)
current_user = user_logic.getByCurrentAccount()
# pylint: disable=E1103
if not current_user or current_user.user_id != user_id:
# this user is not the current user
raise forms.ValidationError('This user is not you.')
return current_user if as_user else user_id
return wrapped
def clean_user_not_exist(field_name):
"""Check if the field_name value is a valid link_id and a user with the
link id does not exist.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method."""
user_id = clean_link_id(field_name)(self)
user = user_model.User.get_by_id(user_id)
if user:
# user exists already
raise forms.ValidationError('There is already a user with this username.')
return user_id
return wrapped
def clean_users_not_same(field_name):
"""Check if the field_name field is a valid user and is not
equal to the current user.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
clean_user_field = clean_existing_user(field_name)
user = clean_user_field(self)
current_user = user_logic.getByCurrentAccount()
# pylint: disable=E1103
if user.key == current_user.key:
# users are equal
raise forms.ValidationError('You cannot enter yourself here.')
return user
return wrapped
def cleanValidAddressCharacters(value):
"""Cleaning method for a value that is submitted as a part of an address
information. It ensures that it complies with Google's character
requirements for shipping purposes.
Args:
value: The value submitted in the form as a part of an address.
Returns:
Cleaned value for the field.
Raises:
django_forms.ValidationError if the submitted value contains invalid
characters.
"""
if value and not DEF_VALID_SHIPPING_CHARS.match(value):
raise forms.ValidationError(
safestring.mark_safe(DEF_INVALID_SHIPPING_CHARS))
else:
return value
def clean_content_length(field_name, min_length=0, max_length=500):
"""Clean method for cleaning a field which must contain at least min and
not more then max length characters.
Args:
field_name: the name of the field needed cleaning
min_length: the minimum amount of allowed characters
max_length: the maximum amount of allowed characters
"""
@check_field_is_empty(field_name)
def wrapper(self):
"""Decorator wrapper method.
"""
value = self.cleaned_data[field_name]
value_length = len(value)
if value_length < min_length:
raise forms.ValidationError(DEF_MUST_BE_ABOVE_LIMIT %(
min_length, value_length))
if value_length > max_length:
raise forms.ValidationError(DEF_MUST_BE_UNDER_LIMIT %(
max_length, value_length))
return value
return wrapper
def clean_phone_number(field_name):
"""Clean method for cleaning a field that may only contain numerical values.
"""
@check_field_is_empty(field_name)
def wrapper(self):
"""Decorator wrapped method.
"""
value = self.cleaned_data.get(field_name)
# allow for a '+' prefix which means '00'
if value[0] == '+':
value = '00' + value[1:]
if not value.isdigit():
raise forms.ValidationError('Only numerical characters are allowed')
return value
return wrapper
def clean_feed_url(field_name):
"""Clean method for cleaning feed url.
"""
def wrapper(self):
"""Decorator wrapped method.
"""
feed_url = self.cleaned_data.get(field_name)
if feed_url == '':
# feed url not supplied (which is OK), so do not try to validate it
return None
if not validate.isFeedURLValid(feed_url):
raise forms.ValidationError('This URL is not a valid ATOM or RSS feed.')
return feed_url
return wrapper
def sanitize_html_string(content):
"""Sanitizes the given html string.
Raises:
forms.ValidationError in case of an error.
"""
try:
parser = html5lib.HTMLParser(tokenizer=sanitizer.HTMLSanitizer)
parsed = parser.parseFragment(content, encoding='utf-8')
cleaned_content = ''.join([tag.toxml() for tag in parsed.childNodes])
except (HTMLParser.HTMLParseError, html5parser.ParseError) as msg:
raise forms.ValidationError(msg)
return cleaned_content
def clean_html_content(field_name):
"""Clean method for cleaning HTML content.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
content = self.cleaned_data.get(field_name)
# clean_html_content is called when writing data into GAE rather than
# when reading data from GAE. This short-circuiting of the sanitizer
# only affects html authored by developers. The is_current_user_admin test
# for example allows developers to add javascript.
if users.is_current_user_admin():
return content
content = sanitize_html_string(content)
return content
return wrapped
def clean_url(field_name):
"""Clean method for cleaning a field belonging to a LinkProperty.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
value = self.cleaned_data.get(field_name)
validator = validators.URLValidator()
# call the Django URLField cleaning method to
# properly clean/validate this field
try:
validator(value)
except forms.ValidationError as e:
if e.code == 'invalid':
msg = translation.ugettext(u'Enter a valid URL.')
raise forms.ValidationError(msg, code='invalid')
return value
return wrapped
def clean_irc(field_name):
"""Clean method for cleaning an irc field.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
value = self.cleaned_data.get(field_name)
validator = validators.URLValidator()
to_clean = value
if value.startswith('irc://'):
to_clean = value.replace('irc://', 'http://', 1)
# call the Django URLField cleaning method to
# properly clean/validate this field
try:
validator(to_clean)
except forms.ValidationError as e:
if e.code == 'invalid':
msg = translation.ugettext(u'Enter a valid URL or irc:// url.')
raise forms.ValidationError(msg, code='invalid')
return value
return wrapped
def clean_mailto(field_name):
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
value = self.cleaned_data.get(field_name)
validator = validators.URLValidator()
to_clean = value
if value.startswith('mailto:'):
to_clean = value.replace('mailto:', '', 1)
validator = validators.validate_email
# call the Django URLField cleaning method to
# properly clean/validate this field
try:
validator(to_clean)
except forms.ValidationError as e:
if e.code == 'invalid':
msg = translation.ugettext(u'Enter a valid URL or mailto: link.')
raise forms.ValidationError(msg, code='invalid')
return value
return wrapped
def str2set(string_field, separator=','):
"""Clean method for cleaning comma separated strings.
Obtains the separated string from the form and returns it as
a set of strings.
"""
def wrapper(self):
"""Decorator wrapper method.
"""
cleaned_data = self.cleaned_data
string_data = cleaned_data.get(string_field)
list_data = []
for string in string_data.split(separator):
string_strip = string.strip()
if string_strip and string_strip not in list_data:
list_data.append(string_strip)
return list_data
return wrapper