A better email validator which actually validates the email address.
[mailman.git] / src / mailman / rest / validator.py
blob1d5ad4ef927c49df4aee8c594b440c60a0cf298d
1 # Copyright (C) 2010-2015 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """REST web form validation."""
20 __all__ = [
21 'PatchValidator',
22 'Validator',
23 'enum_validator',
24 'language_validator',
25 'list_of_strings_validator',
26 'subscriber_validator',
30 from mailman.core.errors import (
31 ReadOnlyPATCHRequestError, UnknownPATCHRequestError)
32 from mailman.interfaces.address import IEmailValidator
33 from mailman.interfaces.languages import ILanguageManager
34 from uuid import UUID
35 from zope.component import getUtility
38 COMMASPACE = ', '
42 class enum_validator:
43 """Convert an enum value name into an enum value."""
45 def __init__(self, enum_class):
46 self._enum_class = enum_class
48 def __call__(self, enum_value):
49 # This will raise a KeyError if the enum value is unknown. The
50 # Validator API requires turning this into a ValueError.
51 try:
52 return self._enum_class[enum_value]
53 except KeyError as exception:
54 # Retain the error message.
55 raise ValueError(exception.args[0])
58 def subscriber_validator(subscriber):
59 """Convert an email-or-int to an email-or-UUID."""
60 try:
61 return UUID(int=int(subscriber))
62 except ValueError:
63 # It must be an email address.
64 if getUtility(IEmailValidator).is_valid(subscriber):
65 return subscriber
66 raise ValueError
69 def language_validator(code):
70 """Convert a language code to a Language object."""
71 return getUtility(ILanguageManager)[code]
74 def list_of_strings_validator(values):
75 """Turn a list of things, or a single thing, into a list of unicodes."""
76 if not isinstance(values, (list, tuple)):
77 values = [values]
78 for value in values:
79 if not isinstance(value, str):
80 raise ValueError('Expected str, got {!r}'.format(value))
81 return values
85 class Validator:
86 """A validator of parameter input."""
88 def __init__(self, **kws):
89 if '_optional' in kws:
90 self._optional = set(kws.pop('_optional'))
91 else:
92 self._optional = set()
93 self._converters = kws.copy()
95 def __call__(self, request):
96 values = {}
97 extras = set()
98 cannot_convert = set()
99 form_data = {}
100 # All keys which show up only once in the form data get a scalar value
101 # in the pre-converted dictionary. All keys which show up more than
102 # once get a list value.
103 missing = object()
104 items = request.params.items()
105 for key, new_value in items:
106 old_value = form_data.get(key, missing)
107 if old_value is missing:
108 form_data[key] = new_value
109 elif isinstance(old_value, list):
110 old_value.append(new_value)
111 else:
112 form_data[key] = [old_value, new_value]
113 # Now do all the conversions.
114 for key, value in form_data.items():
115 try:
116 values[key] = self._converters[key](value)
117 except KeyError:
118 extras.add(key)
119 except (TypeError, ValueError):
120 cannot_convert.add(key)
121 # Make sure there are no unexpected values.
122 if len(extras) != 0:
123 extras = COMMASPACE.join(sorted(extras))
124 raise ValueError('Unexpected parameters: {0}'.format(extras))
125 # Make sure everything could be converted.
126 if len(cannot_convert) != 0:
127 bad = COMMASPACE.join(sorted(cannot_convert))
128 raise ValueError('Cannot convert parameters: {0}'.format(bad))
129 # Make sure nothing's missing.
130 value_keys = set(values)
131 required_keys = set(self._converters) - self._optional
132 if value_keys & required_keys != required_keys:
133 missing = COMMASPACE.join(sorted(required_keys - value_keys))
134 raise ValueError('Missing parameters: {0}'.format(missing))
135 return values
137 def update(self, obj, request):
138 """Update the object with the values in the request.
140 This first validates and converts the attributes in the request, then
141 updates the given object with the newly converted values.
143 :param obj: The object to update.
144 :type obj: object
145 :param request: The HTTP request.
146 :raises ValueError: if conversion failed for some attribute.
148 for key, value in self.__call__(request).items():
149 self._converters[key].put(obj, key, value)
153 class PatchValidator(Validator):
154 """Create a special validator for PATCH requests.
156 PATCH is different than PUT because with the latter, you're changing the
157 entire resource, so all expected attributes must exist. With the former,
158 you're only changing a subset of the attributes, so you only validate the
159 ones that exist in the request.
161 def __init__(self, request, converters):
162 """Create a validator for the PATCH request.
164 :param request: The request object, which must have a .PATCH
165 attribute.
166 :param converters: A mapping of attribute names to the converter for
167 that attribute's type. Generally, this will be a GetterSetter
168 instance, but it might be something more specific for custom data
169 types (e.g. non-basic types like unicodes).
170 :raises UnknownPATCHRequestError: if the request contains an unknown
171 attribute, i.e. one that is not in the `attributes` mapping.
172 :raises ReadOnlyPATCHRequest: if the requests contains an attribute
173 that is defined as read-only.
175 validationators = {}
176 for attribute in request.params:
177 if attribute not in converters:
178 raise UnknownPATCHRequestError(attribute)
179 if converters[attribute].decoder is None:
180 raise ReadOnlyPATCHRequestError(attribute)
181 validationators[attribute] = converters[attribute]
182 super(PatchValidator, self).__init__(**validationators)