Imported Upstream version 12.1.0
[contrib/python-twisted.git] / twisted / words / protocols / jabber / error.py
1 # -*- test-case-name: twisted.words.test.test_jabbererror -*-
2 #
3 # Copyright (c) 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     'remote-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: C{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