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)
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
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
31 class RESTError(MailmanError
):
32 """Base class for REST API errors."""
36 class UnknownPATCHRequestError(RESTError
):
37 """A PATCH request contained an unknown attribute."""
39 def __init__(self
, attribute
):
40 self
.attribute
= attribute
44 class ReadOnlyPATCHRequestError(RESTError
):
45 """A PATCH request contained a read-only attribute."""
47 def __init__(self
, attribute
):
48 self
.attribute
= attribute
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
:
65 return self
._enum
_class
[enum_value
]
66 except KeyError as exception
:
67 # Retain the error message.
68 raise ValueError(exception
.args
[0])
72 def subscriber_validator(api
):
73 """Convert an email-or-(int|hex) to an email-or-UUID."""
74 def _inner(subscriber
):
76 return api
.to_uuid(subscriber
)
78 # It must be an email address.
79 if getUtility(IEmailValidator
).is_valid(subscriber
):
86 def language_validator(code
):
87 """Convert a language code to a Language object."""
88 return getUtility(ILanguageManager
)[code
]
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)):
97 if not isinstance(value
, str):
98 raise ValueError('Expected str, got {!r}'.format(value
))
104 """A validator of parameter input."""
106 def __init__(self
, **kws
):
107 if '_optional' in kws
:
108 self
._optional
= set(kws
.pop('_optional'))
110 self
._optional
= set()
111 self
._converters
= kws
.copy()
113 def __call__(self
, request
):
116 cannot_convert
= set()
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.
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
)
130 form_data
[key
] = [old_value
, new_value
]
131 # Now do all the conversions.
132 for key
, value
in form_data
.items():
134 values
[key
] = self
._converters
[key
](value
)
137 except (TypeError, ValueError):
138 cannot_convert
.add(key
)
139 # Make sure there are no unexpected values.
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
))
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.
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
)
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
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.
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
)