''' This module sets up a scheme for validating that arbitrary Python objects are correctly typed. It is totally decoupled from Django, composable, easily wrapped, and easily extended. A validator takes two parameters--var_name and val--and returns an error if val is not the correct type. The var_name parameter is used to format error messages. Validators return None when there are no errors. Example primitive validators are check_string, check_int, and check_bool. Compound validators are created by check_list and check_dict. Note that those functions aren't directly called for validation; instead, those functions are called to return other functions that adhere to the validator contract. This is similar to how Python decorators are often parameterized. The contract for check_list and check_dict is that they get passed in other validators to apply to their items. This allows you to build up validators for arbitrarily complex validators. See ValidatorTestCase for example usage. A simple example of composition is this: check_list(check_string)('my_list', ['a', 'b', 'c']) is None To extend this concept, it's simply a matter of writing your own validator for any particular type of object. ''' import re import ujson from django.utils.translation import ugettext as _ from django.core.exceptions import ValidationError from django.core.validators import validate_email, URLValidator from typing import Iterable, Optional, Tuple, cast, List from datetime import datetime from zerver.lib.request import JsonableError from zerver.lib.types import Validator, ProfileFieldData def check_string(var_name: str, val: object) -> Optional[str]: if not isinstance(val, str): return _('%s is not a string') % (var_name,) return None def check_required_string(var_name: str, val: object) -> Optional[str]: error = check_string(var_name, val) if error: return error val = cast(str, val) if not val.strip(): return _("{item} cannot be blank.").format(item=var_name) return None def check_short_string(var_name: str, val: object) -> Optional[str]: return check_capped_string(50)(var_name, val) def check_capped_string(max_length: int) -> Validator: def validator(var_name: str, val: object) -> Optional[str]: if not isinstance(val, str): return _('%s is not a string') % (var_name,) if len(val) > max_length: return _("{var_name} is too long (limit: {max_length} characters)").format( var_name=var_name, max_length=max_length) return None return validator def check_string_fixed_length(length: int) -> Validator: def validator(var_name: str, val: object) -> Optional[str]: if not isinstance(val, str): return _('%s is not a string') % (var_name,) if len(val) != length: return _("{var_name} has incorrect length {length}; should be {target_length}").format( var_name=var_name, target_length=length, length=len(val)) return None return validator def check_long_string(var_name: str, val: object) -> Optional[str]: return check_capped_string(500)(var_name, val) def check_date(var_name: str, val: object) -> Optional[str]: if not isinstance(val, str): return _('%s is not a string') % (var_name,) try: datetime.strptime(val, '%Y-%m-%d') except ValueError: return _('%s is not a date') % (var_name,) return None def check_int(var_name: str, val: object) -> Optional[str]: if not isinstance(val, int): return _('%s is not an integer') % (var_name,) return None def check_int_in(possible_values: List[int]) -> Validator: def validator(var_name: str, val: object) -> Optional[str]: not_int = check_int(var_name, val) if not_int is not None: return not_int if val not in possible_values: return _("Invalid %s") % (var_name,) return None return validator def check_float(var_name: str, val: object) -> Optional[str]: if not isinstance(val, float): return _('%s is not a float') % (var_name,) return None def check_bool(var_name: str, val: object) -> Optional[str]: if not isinstance(val, bool): return _('%s is not a boolean') % (var_name,) return None def check_color(var_name: str, val: object) -> Optional[str]: if not isinstance(val, str): return _('%s is not a string') % (var_name,) valid_color_pattern = re.compile(r'^#([a-fA-F0-9]{3,6})$') matched_results = valid_color_pattern.match(val) if not matched_results: return _('%s is not a valid hex color code') % (var_name,) return None def check_none_or(sub_validator: Validator) -> Validator: def f(var_name: str, val: object) -> Optional[str]: if val is None: return None else: return sub_validator(var_name, val) return f def check_list(sub_validator: Optional[Validator], length: Optional[int]=None) -> Validator: def f(var_name: str, val: object) -> Optional[str]: if not isinstance(val, list): return _('%s is not a list') % (var_name,) if length is not None and length != len(val): return (_('%(container)s should have exactly %(length)s items') % {'container': var_name, 'length': length}) if sub_validator: for i, item in enumerate(val): vname = '%s[%d]' % (var_name, i) error = sub_validator(vname, item) if error: return error return None return f def check_dict(required_keys: Iterable[Tuple[str, Validator]]=[], optional_keys: Iterable[Tuple[str, Validator]]=[], value_validator: Optional[Validator]=None, _allow_only_listed_keys: bool=False) -> Validator: def f(var_name: str, val: object) -> Optional[str]: if not isinstance(val, dict): return _('%s is not a dict') % (var_name,) for k, sub_validator in required_keys: if k not in val: return (_('%(key_name)s key is missing from %(var_name)s') % {'key_name': k, 'var_name': var_name}) vname = '%s["%s"]' % (var_name, k) error = sub_validator(vname, val[k]) if error: return error for k, sub_validator in optional_keys: if k in val: vname = '%s["%s"]' % (var_name, k) error = sub_validator(vname, val[k]) if error: return error if value_validator: for key in val: vname = '%s contains a value that' % (var_name,) error = value_validator(vname, val[key]) if error: return error if _allow_only_listed_keys: required_keys_set = set(x[0] for x in required_keys) optional_keys_set = set(x[0] for x in optional_keys) delta_keys = set(val.keys()) - required_keys_set - optional_keys_set if len(delta_keys) != 0: return _("Unexpected arguments: %s") % (", ".join(list(delta_keys)),) return None return f def check_dict_only(required_keys: Iterable[Tuple[str, Validator]], optional_keys: Iterable[Tuple[str, Validator]]=[]) -> Validator: return check_dict(required_keys, optional_keys, _allow_only_listed_keys=True) def check_variable_type(allowed_type_funcs: Iterable[Validator]) -> Validator: """ Use this validator if an argument is of a variable type (e.g. processing properties that might be strings or booleans). `allowed_type_funcs`: the check_* validator functions for the possible data types for this variable. """ def enumerated_type_check(var_name: str, val: object) -> Optional[str]: for func in allowed_type_funcs: if not func(var_name, val): return None return _('%s is not an allowed_type') % (var_name,) return enumerated_type_check def equals(expected_val: object) -> Validator: def f(var_name: str, val: object) -> Optional[str]: if val != expected_val: return (_('%(variable)s != %(expected_value)s (%(value)s is wrong)') % {'variable': var_name, 'expected_value': expected_val, 'value': val}) return None return f def validate_login_email(email: str) -> None: try: validate_email(email) except ValidationError as err: raise JsonableError(str(err.message)) def check_url(var_name: str, val: object) -> Optional[str]: # First, ensure val is a string string_msg = check_string(var_name, val) if string_msg is not None: return string_msg # Now, validate as URL validate = URLValidator() try: validate(val) return None except ValidationError: return _('%s is not a URL') % (var_name,) def check_external_account_url_pattern(var_name: str, val: object) -> Optional[str]: error = check_string(var_name, val) if error: return error val = cast(str, val) if val.count('%(username)s') != 1: return _('Malformed URL pattern.') url_val = val.replace('%(username)s', 'username') error = check_url(var_name, url_val) if error: return error return None def validate_choice_field_data(field_data: ProfileFieldData) -> Optional[str]: """ This function is used to validate the data sent to the server while creating/editing choices of the choice field in Organization settings. """ validator = check_dict_only([ ('text', check_required_string), ('order', check_required_string), ]) for key, value in field_data.items(): if not key.strip(): return _("'{item}' cannot be blank.").format(item='value') error = validator('field_data', value) if error: return error return None def validate_choice_field(var_name: str, field_data: str, value: object) -> Optional[str]: """ This function is used to validate the value selected by the user against a choice field. This is not used to validate admin data. """ field_data_dict = ujson.loads(field_data) if value not in field_data_dict: msg = _("'{value}' is not a valid choice for '{field_name}'.") return msg.format(value=value, field_name=var_name) return None def check_widget_content(widget_content: object) -> Optional[str]: if not isinstance(widget_content, dict): return 'widget_content is not a dict' if 'widget_type' not in widget_content: return 'widget_type is not in widget_content' if 'extra_data' not in widget_content: return 'extra_data is not in widget_content' widget_type = widget_content['widget_type'] extra_data = widget_content['extra_data'] if not isinstance(extra_data, dict): return 'extra_data is not a dict' if widget_type == 'zform': if 'type' not in extra_data: return 'zform is missing type field' if extra_data['type'] == 'choices': check_choices = check_list( check_dict([ ('short_name', check_string), ('long_name', check_string), ('reply', check_string), ]), ) checker = check_dict([ ('heading', check_string), ('choices', check_choices), ]) msg = checker('extra_data', extra_data) if msg: return msg return None return 'unknown zform type: ' + extra_data['type'] return 'unknown widget type: ' + widget_type # Converter functions for use with has_request_variables def to_non_negative_int(s: str, max_int_size: int=2**32-1) -> int: x = int(s) if x < 0: raise ValueError("argument is negative") if x > max_int_size: raise ValueError('%s is too large (max %s)' % (x, max_int_size)) return x def check_string_or_int_list(var_name: str, val: object) -> Optional[str]: if isinstance(val, str): return None if not isinstance(val, list): return _('%s is not a string or an integer list') % (var_name,) return check_list(check_int)(var_name, val) def check_string_or_int(var_name: str, val: object) -> Optional[str]: if isinstance(val, str) or isinstance(val, int): return None return _('%s is not a string or integer') % (var_name,)