allow_none -> allow_blank
[mailman.git] / src / mailman / rest / validator.py
blobbcc6b3321252275bc56058fb6ddb51908767d4cd
1 # Copyright (C) 2010-2016 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 from mailman import public
21 from mailman.interfaces.address import IEmailValidator
22 from mailman.interfaces.errors import MailmanError
23 from mailman.interfaces.languages import ILanguageManager
24 from zope.component import getUtility
27 COMMASPACE = ', '
30 @public
31 class RESTError(MailmanError):
32 """Base class for REST API errors."""
35 @public
36 class UnknownPATCHRequestError(RESTError):
37 """A PATCH request contained an unknown attribute."""
39 def __init__(self, attribute):
40 self.attribute = attribute
43 @public
44 class ReadOnlyPATCHRequestError(RESTError):
45 """A PATCH request contained a read-only attribute."""
47 def __init__(self, attribute):
48 self.attribute = attribute
51 @public
52 class enum_validator:
53 """Convert an enum value name into an enum value."""
55 def __init__(self, enum_class, *, allow_blank=False):
56 self._enum_class = enum_class
57 self._allow_blank = allow_blank
59 def __call__(self, enum_value):
60 # This will raise a KeyError if the enum value is unknown. The
61 # Validator API requires turning this into a ValueError.
62 if not enum_value and self._allow_blank:
63 return None
64 try:
65 return self._enum_class[enum_value]
66 except KeyError as exception:
67 # Retain the error message.
68 raise ValueError(exception.args[0])
71 @public
72 def subscriber_validator(api):
73 """Convert an email-or-(int|hex) to an email-or-UUID."""
74 def _inner(subscriber):
75 try:
76 return api.to_uuid(subscriber)
77 except ValueError:
78 # It must be an email address.
79 if getUtility(IEmailValidator).is_valid(subscriber):
80 return subscriber
81 raise ValueError
82 return _inner
85 @public
86 def language_validator(code):
87 """Convert a language code to a Language object."""
88 return getUtility(ILanguageManager)[code]
91 @public
92 def list_of_strings_validator(values):
93 """Turn a list of things, or a single thing, into a list of unicodes."""
94 if not isinstance(values, (list, tuple)):
95 values = [values]
96 for value in values:
97 if not isinstance(value, str):
98 raise ValueError('Expected str, got {!r}'.format(value))
99 return values
102 @public
103 class Validator:
104 """A validator of parameter input."""
106 def __init__(self, **kws):
107 if '_optional' in kws:
108 self._optional = set(kws.pop('_optional'))
109 else:
110 self._optional = set()
111 self._converters = kws.copy()
113 def __call__(self, request):
114 values = {}
115 extras = set()
116 cannot_convert = set()
117 form_data = {}
118 # All keys which show up only once in the form data get a scalar value
119 # in the pre-converted dictionary. All keys which show up more than
120 # once get a list value.
121 missing = object()
122 items = request.params.items()
123 for key, new_value in items:
124 old_value = form_data.get(key, missing)
125 if old_value is missing:
126 form_data[key] = new_value
127 elif isinstance(old_value, list):
128 old_value.append(new_value)
129 else:
130 form_data[key] = [old_value, new_value]
131 # Now do all the conversions.
132 for key, value in form_data.items():
133 try:
134 values[key] = self._converters[key](value)
135 except KeyError:
136 extras.add(key)
137 except (TypeError, ValueError):
138 cannot_convert.add(key)
139 # Make sure there are no unexpected values.
140 if len(extras) != 0:
141 extras = COMMASPACE.join(sorted(extras))
142 raise ValueError('Unexpected parameters: {}'.format(extras))
143 # Make sure everything could be converted.
144 if len(cannot_convert) != 0:
145 bad = COMMASPACE.join(sorted(cannot_convert))
146 raise ValueError('Cannot convert parameters: {}'.format(bad))
147 # Make sure nothing's missing.
148 value_keys = set(values)
149 required_keys = set(self._converters) - self._optional
150 if value_keys & required_keys != required_keys:
151 missing = COMMASPACE.join(sorted(required_keys - value_keys))
152 raise ValueError('Missing parameters: {}'.format(missing))
153 return values
155 def update(self, obj, request):
156 """Update the object with the values in the request.
158 This first validates and converts the attributes in the request, then
159 updates the given object with the newly converted values.
161 :param obj: The object to update.
162 :type obj: object
163 :param request: The HTTP request.
164 :raises ValueError: if conversion failed for some attribute.
166 for key, value in self.__call__(request).items():
167 self._converters[key].put(obj, key, value)
170 @public
171 class PatchValidator(Validator):
172 """Create a special validator for PATCH requests.
174 PATCH is different than PUT because with the latter, you're changing the
175 entire resource, so all expected attributes must exist. With the former,
176 you're only changing a subset of the attributes, so you only validate the
177 ones that exist in the request.
179 def __init__(self, request, converters):
180 """Create a validator for the PATCH request.
182 :param request: The request object, which must have a .PATCH
183 attribute.
184 :param converters: A mapping of attribute names to the converter for
185 that attribute's type. Generally, this will be a GetterSetter
186 instance, but it might be something more specific for custom data
187 types (e.g. non-basic types like unicodes).
188 :raises UnknownPATCHRequestError: if the request contains an unknown
189 attribute, i.e. one that is not in the `attributes` mapping.
190 :raises ReadOnlyPATCHRequest: if the requests contains an attribute
191 that is defined as read-only.
193 validationators = {}
194 for attribute in request.params:
195 if attribute not in converters:
196 raise UnknownPATCHRequestError(attribute)
197 if converters[attribute].decoder is None:
198 raise ReadOnlyPATCHRequestError(attribute)
199 validationators[attribute] = converters[attribute]
200 super().__init__(**validationators)