OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.words.test.test_jabbererror -*- | |
2 # | |
3 # Copyright (c) 2001-2007 Twisted Matrix Laboratories. | |
4 # See LICENSE for details. | |
5 | |
6 """ | |
7 XMPP Error support. | |
8 """ | |
9 | |
10 import copy | |
11 | |
12 from twisted.words.xish import domish | |
13 | |
14 NS_XML = "http://www.w3.org/XML/1998/namespace" | |
15 NS_XMPP_STREAMS = "urn:ietf:params:xml:ns:xmpp-streams" | |
16 NS_XMPP_STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas" | |
17 | |
18 STANZA_CONDITIONS = { | |
19 'bad-request': {'code': '400', 'type': 'modify'}, | |
20 'conflict': {'code': '409', 'type': 'cancel'}, | |
21 'feature-not-implemented': {'code': '501', 'type': 'cancel'}, | |
22 'forbidden': {'code': '403', 'type': 'auth'}, | |
23 'gone': {'code': '302', 'type': 'modify'}, | |
24 'internal-server-error': {'code': '500', 'type': 'wait'}, | |
25 'item-not-found': {'code': '404', 'type': 'cancel'}, | |
26 'jid-malformed': {'code': '400', 'type': 'modify'}, | |
27 'not-acceptable': {'code': '406', 'type': 'modify'}, | |
28 'not-allowed': {'code': '405', 'type': 'cancel'}, | |
29 'not-authorized': {'code': '401', 'type': 'auth'}, | |
30 'payment-required': {'code': '402', 'type': 'auth'}, | |
31 'recipient-unavailable': {'code': '404', 'type': 'wait'}, | |
32 'redirect': {'code': '302', 'type': 'modify'}, | |
33 'registration-required': {'code': '407', 'type': 'auth'}, | |
34 'remote-server-not-found': {'code': '404', 'type': 'cancel'}, | |
35 'remove-server-timeout': {'code': '504', 'type': 'wait'}, | |
36 'resource-constraint': {'code': '500', 'type': 'wait'}, | |
37 'service-unavailable': {'code': '503', 'type': 'cancel'}, | |
38 'subscription-required': {'code': '407', 'type': 'auth'}, | |
39 'undefined-condition': {'code': '500', 'type': None}, | |
40 'unexpected-request': {'code': '400', 'type': 'wait'}, | |
41 } | |
42 | |
43 CODES_TO_CONDITIONS = { | |
44 '302': ('gone', 'modify'), | |
45 '400': ('bad-request', 'modify'), | |
46 '401': ('not-authorized', 'auth'), | |
47 '402': ('payment-required', 'auth'), | |
48 '403': ('forbidden', 'auth'), | |
49 '404': ('item-not-found', 'cancel'), | |
50 '405': ('not-allowed', 'cancel'), | |
51 '406': ('not-acceptable', 'modify'), | |
52 '407': ('registration-required', 'auth'), | |
53 '408': ('remote-server-timeout', 'wait'), | |
54 '409': ('conflict', 'cancel'), | |
55 '500': ('internal-server-error', 'wait'), | |
56 '501': ('feature-not-implemented', 'cancel'), | |
57 '502': ('service-unavailable', 'wait'), | |
58 '503': ('service-unavailable', 'cancel'), | |
59 '504': ('remote-server-timeout', 'wait'), | |
60 '510': ('service-unavailable', 'cancel'), | |
61 } | |
62 | |
63 class BaseError(Exception): | |
64 """ | |
65 Base class for XMPP error exceptions. | |
66 | |
67 @cvar namespace: The namespace of the C{error} element generated by | |
68 C{getElement}. | |
69 @type namespace: C{str} | |
70 @ivar condition: The error condition. The valid values are defined by | |
71 subclasses of L{BaseError}. | |
72 @type contition: C{str} | |
73 @ivar text: Optional text message to supplement the condition or application | |
74 specific condition. | |
75 @type text: C{unicode} | |
76 @ivar textLang: Identifier of the language used for the message in C{text}. | |
77 Values are as described in RFC 3066. | |
78 @type textLang: C{str} | |
79 @ivar appCondition: Application specific condition element, supplementing | |
80 the error condition in C{condition}. | |
81 @type appCondition: object providing L{domish.IElement}. | |
82 """ | |
83 | |
84 namespace = None | |
85 | |
86 def __init__(self, condition, text=None, textLang=None, appCondition=None): | |
87 Exception.__init__(self) | |
88 self.condition = condition | |
89 self.text = text | |
90 self.textLang = textLang | |
91 self.appCondition = appCondition | |
92 | |
93 | |
94 def __str__(self): | |
95 message = "%s with condition %r" % (self.__class__.__name__, | |
96 self.condition) | |
97 | |
98 if self.text: | |
99 message += ': ' + self.text | |
100 | |
101 return message | |
102 | |
103 | |
104 def getElement(self): | |
105 """ | |
106 Get XML representation from self. | |
107 | |
108 The method creates an L{domish} representation of the | |
109 error data contained in this exception. | |
110 | |
111 @rtype: L{domish.Element} | |
112 """ | |
113 error = domish.Element((None, 'error')) | |
114 error.addElement((self.namespace, self.condition)) | |
115 if self.text: | |
116 text = error.addElement((self.namespace, 'text'), | |
117 content=self.text) | |
118 if self.textLang: | |
119 text[(NS_XML, 'lang')] = self.textLang | |
120 if self.appCondition: | |
121 error.addChild(self.appCondition) | |
122 return error | |
123 | |
124 | |
125 | |
126 class StreamError(BaseError): | |
127 """ | |
128 Stream Error exception. | |
129 | |
130 Refer to RFC 3920, section 4.7.3, for the allowed values for C{condition}. | |
131 """ | |
132 | |
133 namespace = NS_XMPP_STREAMS | |
134 | |
135 def getElement(self): | |
136 """ | |
137 Get XML representation from self. | |
138 | |
139 Overrides the base L{BaseError.getElement} to make sure the returned | |
140 element is in the XML Stream namespace. | |
141 | |
142 @rtype: L{domish.Element} | |
143 """ | |
144 from twisted.words.protocols.jabber.xmlstream import NS_STREAMS | |
145 | |
146 error = BaseError.getElement(self) | |
147 error.uri = NS_STREAMS | |
148 return error | |
149 | |
150 | |
151 | |
152 class StanzaError(BaseError): | |
153 """ | |
154 Stanza Error exception. | |
155 | |
156 Refer to RFC 3920, section 9.3, for the allowed values for C{condition} and | |
157 C{type}. | |
158 | |
159 @ivar type: The stanza error type. Gives a suggestion to the recipient | |
160 of the error on how to proceed. | |
161 @type type: C{str} | |
162 @ivar code: A numeric identifier for the error condition for backwards | |
163 compatibility with pre-XMPP Jabber implementations. | |
164 """ | |
165 | |
166 namespace = NS_XMPP_STANZAS | |
167 | |
168 def __init__(self, condition, type=None, text=None, textLang=None, | |
169 appCondition=None): | |
170 BaseError.__init__(self, condition, text, textLang, appCondition) | |
171 | |
172 if type is None: | |
173 try: | |
174 type = STANZA_CONDITIONS[condition]['type'] | |
175 except KeyError: | |
176 pass | |
177 self.type = type | |
178 | |
179 try: | |
180 self.code = STANZA_CONDITIONS[condition]['code'] | |
181 except KeyError: | |
182 self.code = None | |
183 | |
184 self.children = [] | |
185 self.iq = None | |
186 | |
187 | |
188 def getElement(self): | |
189 """ | |
190 Get XML representation from self. | |
191 | |
192 Overrides the base L{BaseError.getElement} to make sure the returned | |
193 element has a C{type} attribute and optionally a legacy C{code} | |
194 attribute. | |
195 | |
196 @rtype: L{domish.Element} | |
197 """ | |
198 error = BaseError.getElement(self) | |
199 error['type'] = self.type | |
200 if self.code: | |
201 error['code'] = self.code | |
202 return error | |
203 | |
204 | |
205 def toResponse(self, stanza): | |
206 """ | |
207 Construct error response stanza. | |
208 | |
209 The C{stanza} is transformed into an error response stanza by | |
210 swapping the C{to} and C{from} addresses and inserting an error | |
211 element. | |
212 | |
213 @note: This creates a shallow copy of the list of child elements of the | |
214 stanza. The child elements themselves are not copied themselves, | |
215 and references to their parent element will still point to the | |
216 original stanza element. | |
217 | |
218 The serialization of an element does not use the reference to | |
219 its parent, so the typical use case of immediately sending out | |
220 the constructed error response is not affected. | |
221 | |
222 @param stanza: the stanza to respond to | |
223 @type stanza: L{domish.Element} | |
224 """ | |
225 from twisted.words.protocols.jabber.xmlstream import toResponse | |
226 response = toResponse(stanza, stanzaType='error') | |
227 response.children = copy.copy(stanza.children) | |
228 response.addChild(self.getElement()) | |
229 return response | |
230 | |
231 | |
232 def _getText(element): | |
233 for child in element.children: | |
234 if isinstance(child, basestring): | |
235 return unicode(child) | |
236 | |
237 return None | |
238 | |
239 | |
240 | |
241 def _parseError(error, errorNamespace): | |
242 """ | |
243 Parses an error element. | |
244 | |
245 @param error: The error element to be parsed | |
246 @type error: L{domish.Element} | |
247 @param errorNamespace: The namespace of the elements that hold the error | |
248 condition and text. | |
249 @type errorNamespace: C{str} | |
250 @return: Dictionary with extracted error information. If present, keys | |
251 C{condition}, C{text}, C{textLang} have a string value, | |
252 and C{appCondition} has an L{domish.Element} value. | |
253 @rtype: L{dict} | |
254 """ | |
255 condition = None | |
256 text = None | |
257 textLang = None | |
258 appCondition = None | |
259 | |
260 for element in error.elements(): | |
261 if element.uri == errorNamespace: | |
262 if element.name == 'text': | |
263 text = _getText(element) | |
264 textLang = element.getAttribute((NS_XML, 'lang')) | |
265 else: | |
266 condition = element.name | |
267 else: | |
268 appCondition = element | |
269 | |
270 return { | |
271 'condition': condition, | |
272 'text': text, | |
273 'textLang': textLang, | |
274 'appCondition': appCondition, | |
275 } | |
276 | |
277 | |
278 | |
279 def exceptionFromStreamError(element): | |
280 """ | |
281 Build an exception object from a stream error. | |
282 | |
283 @param element: the stream error | |
284 @type element: L{domish.Element} | |
285 @return: the generated exception object | |
286 @rtype: L{StreamError} | |
287 """ | |
288 error = _parseError(element, NS_XMPP_STREAMS) | |
289 | |
290 exception = StreamError(error['condition'], | |
291 error['text'], | |
292 error['textLang'], | |
293 error['appCondition']) | |
294 | |
295 return exception | |
296 | |
297 | |
298 | |
299 def exceptionFromStanza(stanza): | |
300 """ | |
301 Build an exception object from an error stanza. | |
302 | |
303 @param stanza: the error stanza | |
304 @type stanza: L{domish.Element} | |
305 @return: the generated exception object | |
306 @rtype: L{StanzaError} | |
307 """ | |
308 children = [] | |
309 condition = text = textLang = appCondition = type = code = None | |
310 | |
311 for element in stanza.elements(): | |
312 if element.name == 'error' and element.uri == stanza.uri: | |
313 code = element.getAttribute('code') | |
314 type = element.getAttribute('type') | |
315 error = _parseError(element, NS_XMPP_STANZAS) | |
316 condition = error['condition'] | |
317 text = error['text'] | |
318 textLang = error['textLang'] | |
319 appCondition = error['appCondition'] | |
320 | |
321 if not condition and code: | |
322 condition, type = CODES_TO_CONDITIONS[code] | |
323 text = _getText(stanza.error) | |
324 else: | |
325 children.append(element) | |
326 | |
327 if condition is None: | |
328 # TODO: raise exception instead? | |
329 return StanzaError(None) | |
330 | |
331 exception = StanzaError(condition, type, text, textLang, appCondition) | |
332 | |
333 exception.children = children | |
334 exception.stanza = stanza | |
335 | |
336 return exception | |
OLD | NEW |