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)
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."""
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
35 from zope
.component
import getUtility
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.
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."""
61 return UUID(int=int(subscriber
))
63 # It must be an email address.
64 if getUtility(IEmailValidator
).is_valid(subscriber
):
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)):
79 if not isinstance(value
, str):
80 raise ValueError('Expected str, got {!r}'.format(value
))
86 """A validator of parameter input."""
88 def __init__(self
, **kws
):
89 if '_optional' in kws
:
90 self
._optional
= set(kws
.pop('_optional'))
92 self
._optional
= set()
93 self
._converters
= kws
.copy()
95 def __call__(self
, request
):
98 cannot_convert
= set()
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.
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
)
112 form_data
[key
] = [old_value
, new_value
]
113 # Now do all the conversions.
114 for key
, value
in form_data
.items():
116 values
[key
] = self
._converters
[key
](value
)
119 except (TypeError, ValueError):
120 cannot_convert
.add(key
)
121 # Make sure there are no unexpected values.
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
))
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.
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
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.
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
)