pyxmpp.error

Package pyxmpp :: Module error |
[hide private]

# Source Code for Module pyxmpp.error

      1  #
      2  # (C) Copyright 2003-2010 Jacek Konieczny <jajcus@jajcus.net>
      3  #
      4  # This program is free software; you can redistribute it and/or modify
      5  # it under the terms of the GNU Lesser General Public License Version
      6  # 2.1 as published by the Free Software Foundation.
      7  #
      8  # This program is distributed in the hope that it will be useful,
      9  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     10  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     11  # GNU Lesser General Public License for more details.
     12  #
     13  # You should have received a copy of the GNU Lesser General Public
     14  # License along with this program; if not, write to the Free Software
     15  # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
     16  #
     17
     18  """XMPP error handling.
     19
     20  Normative reference:
     21    - `RFC 3920 <http://www.ietf.org/rfc/rfc3920.txt>`__
     22    - `JEP 86 <http://www.jabber.org/jeps/jep-0086.html>`__
     23  """
     24
     25  __docformat__="restructuredtext en"
     26
     27  import libxml2
     28
     29  from pyxmpp.utils import from_utf8, to_utf8
     30  from pyxmpp.xmlextra import common_doc, common_root, common_ns
     31  from pyxmpp import xmlextra
     32  from pyxmpp.exceptions import ProtocolError
     33
     34  stream_errors={
     35              u"bad-format":
     36                  ("Received XML cannot be processed",),
     37              u"bad-namespace-prefix":
     38                  ("Bad namespace prefix",),
     39              u"conflict":
     40                  ("Closing stream because of conflicting stream being opened",),
     41              u"connection-timeout":
     42                  ("Connection was idle too long",),
     43              u"host-gone":
     44                  ("Hostname is no longer hosted on the server",),
     45              u"host-unknown":
     46                  ("Hostname requested is not known to the server",),
     47              u"improper-addressing":
     48                  ("Improper addressing",),
     49              u"internal-server-error":
     50                  ("Internal server error",),
     51              u"invalid-from":
     52                  ("Invalid sender address",),
     53              u"invalid-id":
     54                  ("Invalid stream ID",),
     55              u"invalid-namespace":
     56                  ("Invalid namespace",),
     57              u"invalid-xml":
     58                  ("Invalid XML",),
     59              u"not-authorized":
     60                  ("Not authorized",),
     61              u"policy-violation":
     62                  ("Local policy violation",),
     63              u"remote-connection-failed":
     64                  ("Remote connection failed",),
     65              u"resource-constraint":
     66                  ("Remote connection failed",),
     67              u"restricted-xml":
     68                  ("Restricted XML received",),
     69              u"see-other-host":
     70                  ("Redirection required",),
     71              u"system-shutdown":
     72                  ("The server is being shut down",),
     73              u"undefined-condition":
     74                  ("Unknown error",),
     75              u"unsupported-encoding":
     76                  ("Unsupported encoding",),
     77              u"unsupported-stanza-type":
     78                  ("Unsupported stanza type",),
     79              u"unsupported-version":
     80                  ("Unsupported protocol version",),
     81              u"xml-not-well-formed":
     82                  ("XML sent by client is not well formed",),
     83      }
     84
     85  stanza_errors={
     86              u"bad-request":
     87                  ("Bad request",
     88                  "modify",400),
     89              u"conflict":
     90                  ("Named session or resource already exists",
     91                  "cancel",409),
     92              u"feature-not-implemented":
     93                  ("Feature requested is not implemented",
     94                  "cancel",501),
     95              u"forbidden":
     96                  ("You are forbidden to perform requested action",
     97                  "auth",403),
     98              u"gone":
     99                  ("Recipient or server can no longer be contacted at this address",
    100                  "modify",302),
    101              u"internal-server-error":
    102                  ("Internal server error",
    103                  "wait",500),
    104              u"item-not-found":
    105                  ("Item not found"
    106                  ,"cancel",404),
    107              u"jid-malformed":
    108                  ("JID malformed",
    109                  "modify",400),
    110              u"not-acceptable":
    111                  ("Requested action is not acceptable",
    112                  "modify",406),
    113              u"not-allowed":
    114                  ("Requested action is not allowed",
    115                  "cancel",405),
    116              u"not-authorized":
    117                  ("Not authorized",
    118                  "auth",401),
    119              u"payment-required":
    120                  ("Payment required",
    121                  "auth",402),
    122              u"recipient-unavailable":
    123                  ("Recipient is not available",
    124                  "wait",404),
    125              u"redirect":
    126                  ("Redirection",
    127                  "modify",302),
    128              u"registration-required":
    129                  ("Registration required",
    130                  "auth",407),
    131              u"remote-server-not-found":
    132                  ("Remote server not found",
    133                  "cancel",404),
    134              u"remote-server-timeout":
    135                  ("Remote server timeout",
    136                  "wait",504),
    137              u"resource-constraint":
    138                  ("Resource constraint",
    139                  "wait",500),
    140              u"service-unavailable":
    141                  ("Service is not available",
    142                  "cancel",503),
    143              u"subscription-required":
    144                  ("Subscription is required",
    145                  "auth",407),
    146              u"undefined-condition":
    147                  ("Unknown error",
    148                  "cancel",500),
    149              u"unexpected-request":
    150                  ("Unexpected request",
    151                  "wait",400),
    152      }
    153
    154  legacy_codes={
    155          302: "redirect",
    156          400: "bad-request",
    157          401: "not-authorized",
    158          402: "payment-required",
    159          403: "forbidden",
    160          404: "item-not-found",
    161          405: "not-allowed",
    162          406: "not-acceptable",
    163          407: "registration-required",
    164          408: "remote-server-timeout",
    165          409: "conflict",
    166          500: "internal-server-error",
    167          501: "feature-not-implemented",
    168          502: "service-unavailable",
    169          503: "service-unavailable",
    170          504: "remote-server-timeout",
    171          510: "service-unavailable",
    172      }
    173
    174  STANZA_ERROR_NS='urn:ietf:params:xml:ns:xmpp-stanzas'
    175  STREAM_ERROR_NS='urn:ietf:params:xml:ns:xmpp-streams'
    176  PYXMPP_ERROR_NS='http://pyxmpp.jajcus.net/xmlns/errors'
    177  STREAM_NS="http://etherx.jabber.org/streams"
    178
    179 -class ErrorNode:
    180      """Base class for both XMPP stream and stanza errors"""
    181 -    def __init__(self,xmlnode_or_cond,ns=None,copy=True,parent=None):
    182          """Initialize an ErrorNode object.
    183
    184          :Parameters:
    185              - `xmlnode_or_cond`: XML node to be wrapped into this object
    186                or error condition name.
    187              - `ns`: XML namespace URI of the error condition element (to be
    188                used when the provided node has no namespace).
    189              - `copy`: When `True` then the XML node will be copied,
    190                otherwise it is only borrowed.
    191              - `parent`: Parent node for the XML node to be copied or created.
    192          :Types:
    193              - `xmlnode_or_cond`: `libxml2.xmlNode` or `unicode`
    194              - `ns`: `unicode`
    195              - `copy`: `bool`
    196              - `parent`: `libxml2.xmlNode`"""
    197          if type(xmlnode_or_cond) is str:
    198              xmlnode_or_cond=unicode(xmlnode_or_cond,"utf-8")
    199          self.xmlnode=None
    200          self.borrowed=0
    201          if isinstance(xmlnode_or_cond,libxml2.xmlNode):
    202              self.__from_xml(xmlnode_or_cond,ns,copy,parent)
    203          elif isinstance(xmlnode_or_cond,ErrorNode):
    204              if not copy:
    205                  raise TypeError, "ErrorNodes may only be copied"
    206              self.ns=from_utf8(xmlnode_or_cond.ns.getContent())
    207              self.xmlnode=xmlnode_or_cond.xmlnode.docCopyNode(common_doc,1)
    208              if not parent:
    209                  parent=common_root
    210              parent.addChild(self.xmlnode)
    211          elif ns is None:
    212              raise ValueError, "Condition namespace not given"
    213          else:
    214              if parent:
    215                  self.xmlnode=parent.newChild(common_ns,"error",None)
    216                  self.borrowed=1
    217              else:
    218                  self.xmlnode=common_root.newChild(common_ns,"error",None)
    219              cond=self.xmlnode.newChild(None,to_utf8(xmlnode_or_cond),None)
    220              ns=cond.newNs(ns,None)
    221              cond.setNs(ns)
    222              self.ns=from_utf8(ns.getContent())
    223
    224 -    def __from_xml(self,xmlnode,ns,copy,parent):
    225          """Initialize an ErrorNode object from an XML node.
    226
    227          :Parameters:
    228              - `xmlnode`: XML node to be wrapped into this object.
    229              - `ns`: XML namespace URI of the error condition element (to be
    230                used when the provided node has no namespace).
    231              - `copy`: When `True` then the XML node will be copied,
    232                otherwise it is only borrowed.
    233              - `parent`: Parent node for the XML node to be copied or created.
    234          :Types:
    235              - `xmlnode`: `libxml2.xmlNode`
    236              - `ns`: `unicode`
    237              - `copy`: `bool`
    238              - `parent`: `libxml2.xmlNode`"""
    239          if not ns:
    240              ns=None
    241              c=xmlnode.children
    242              while c:
    243                  ns=c.ns().getContent()
    244                  if ns in (STREAM_ERROR_NS,STANZA_ERROR_NS):
    245                      break
    246                  ns=None
    247                  c=c.next
    248              if ns==None:
    249                  raise ProtocolError, "Bad error namespace"
    250          self.ns=from_utf8(ns)
    251          if copy:
    252              self.xmlnode=xmlnode.docCopyNode(common_doc,1)
    253              if not parent:
    254                  parent=common_root
    255              parent.addChild(self.xmlnode)
    256          else:
    257              self.xmlnode=xmlnode
    258              self.borrowed=1
    259          if copy:
    260              ns1=xmlnode.ns()
    261              xmlextra.replace_ns(self.xmlnode, ns1, common_ns)
    262
    263 -    def __del__(self):
    264          if self.xmlnode:
    265              self.free()
    266
    267 -    def free(self):
    268          """Free the associated XML node."""
    269          if not self.borrowed:
    270              self.xmlnode.unlinkNode()
    271              self.xmlnode.freeNode()
    272          self.xmlnode=None
    273
    274 -    def free_borrowed(self):
    275          """Free the associated "borrowed" XML node."""
    276          self.xmlnode=None
    277
    278 -    def is_legacy(self):
    279          """Check if the error node is a legacy error element.
    280
    281          :return: `True` if it is a legacy error.
    282          :returntype: `bool`"""
    283          return not self.xmlnode.hasProp("type")
    284
    285 -    def xpath_eval(self,expr,namespaces=None):
    286          """Evaluate XPath expression on the error element.
    287
    288          The expression will be evaluated in context where the common namespace
    289          (the one used for stanza elements, mapped to 'jabber:client',
    290          'jabber:server', etc.) is bound to prefix "ns" and other namespaces are
    291          bound accordingly to the `namespaces` list.
    292
    293          :Parameters:
    294              - `expr`: the XPath expression.
    295              - `namespaces`: prefix to namespace mapping.
    296          :Types:
    297              - `expr`: `unicode`
    298              - `namespaces`: `dict`
    299
    300          :return: the result of the expression evaluation.
    301          """
    302          ctxt = common_doc.xpathNewContext()
    303          ctxt.setContextNode(self.xmlnode)
    304          ctxt.xpathRegisterNs("ns",to_utf8(self.ns))
    305          if namespaces:
    306              for prefix,uri in namespaces.items():
    307                  ctxt.xpathRegisterNs(prefix,uri)
    308          ret=ctxt.xpathEval(expr)
    309          ctxt.xpathFreeContext()
    310          return ret
    311
    312 -    def get_condition(self,ns=None):
    313          """Get the condition element of the error.
    314
    315          :Parameters:
    316              - `ns`: namespace URI of the condition element if it is not
    317                the XMPP namespace of the error element.
    318          :Types:
    319              - `ns`: `unicode`
    320
    321          :return: the condition element or `None`.
    322          :returntype: `libxml2.xmlNode`"""
    323          if ns is None:
    324              ns=self.ns
    325          c=self.xpath_eval("ns:*")
    326          if not c:
    327              self.upgrade()
    328              c=self.xpath_eval("ns:*")
    329          if not c:
    330              return None
    331          if ns==self.ns and c[0].name=="text":
    332              if len(c)==1:
    333                  return None
    334              c=c[1:]
    335          return c[0]
    336
    337 -    def get_text(self):
    338          """Get the description text from the error element.
    339
    340          :return: the text provided with the error or `None`.
    341          :returntype: `unicode`"""
    342          c=self.xpath_eval("ns:*")
    343          if not c:
    344              self.upgrade()
    345          t=self.xpath_eval("ns:text")
    346          if not t:
    347              return None
    348          return from_utf8(t[0].getContent())
    349
    350 -    def add_custom_condition(self,ns,cond,content=None):
    351          """Add custom condition element to the error.
    352
    353          :Parameters:
    354              - `ns`: namespace URI.
    355              - `cond`: condition name.
    356              - `content`: content of the element.
    357
    358          :Types:
    359              - `ns`: `unicode`
    360              - `cond`: `unicode`
    361              - `content`: `unicode`
    362
    363          :return: the new condition element.
    364          :returntype: `libxml2.xmlNode`"""
    365          c=self.xmlnode.newTextChild(None,to_utf8(cond),content)
    366          ns=c.newNs(to_utf8(ns),None)
    367          c.setNs(ns)
    368          return c
    369
    370 -    def upgrade(self):
    371          """Upgrade a legacy error element to the XMPP compliant one.
    372
    373          Use the error code provided to select the condition and the
    374          <error/> CDATA for the error text."""
    375
    376          if not self.xmlnode.hasProp("code"):
    377              code=None
    378          else:
    379              try:
    380                  code=int(self.xmlnode.prop("code"))
    381              except (ValueError,KeyError):
    382                  code=None
    383
    384          if code and legacy_codes.has_key(code):
    385              cond=legacy_codes[code]
    386          else:
    387              cond=None
    388
    389          condition=self.xpath_eval("ns:*")
    390          if condition:
    391              return
    392          elif cond is None:
    393              condition=self.xmlnode.newChild(None,"undefined-condition",None)
    394              ns=condition.newNs(to_utf8(self.ns),None)
    395              condition.setNs(ns)
    396              condition=self.xmlnode.newChild(None,"unknown-legacy-error",None)
    397              ns=condition.newNs(PYXMPP_ERROR_NS,None)
    398              condition.setNs(ns)
    399          else:
    400              condition=self.xmlnode.newChild(None,cond,None)
    401              ns=condition.newNs(to_utf8(self.ns),None)
    402              condition.setNs(ns)
    403          txt=self.xmlnode.getContent()
    404          if txt:
    405              text=self.xmlnode.newTextChild(None,"text",txt)
    406              ns=text.newNs(to_utf8(self.ns),None)
    407              text.setNs(ns)
    408
    409 -    def downgrade(self):
    410          """Downgrade an XMPP error element to the legacy format.
    411
    412          Add a numeric code attribute according to the condition name."""
    413          if self.xmlnode.hasProp("code"):
    414              return
    415          cond=self.get_condition()
    416          if not cond:
    417              return
    418          cond=cond.name
    419          if stanza_errors.has_key(cond) and stanza_errors[cond][2]:
    420              self.xmlnode.setProp("code",to_utf8(stanza_errors[cond][2]))
    421
    422 -    def serialize(self):
    423          """Serialize the element node.
    424
    425          :return: serialized element in UTF-8 encoding.
    426          :returntype: `str`"""
    427          return self.xmlnode.serialize(encoding="utf-8")
    428
    429 -class StreamErrorNode(ErrorNode):
    430      """Stream error element."""
    431 -    def __init__(self,xmlnode_or_cond,copy=1,parent=None):
    432          """Initialize a StreamErrorNode object.
    433
    434          :Parameters:
    435              - `xmlnode_or_cond`: XML node to be wrapped into this object
    436                or the primary (defined by XMPP specification) error condition name.
    437              - `copy`: When `True` then the XML node will be copied,
    438                otherwise it is only borrowed.
    439              - `parent`: Parent node for the XML node to be copied or created.
    440          :Types:
    441              - `xmlnode_or_cond`: `libxml2.xmlNode` or `unicode`
    442              - `copy`: `bool`
    443              - `parent`: `libxml2.xmlNode`"""
    444          if type(xmlnode_or_cond) is str:
    445              xmlnode_or_cond = xmlnode_or_cond.decode("utf-8")
    446          if type(xmlnode_or_cond) is unicode:
    447              if not stream_errors.has_key(xmlnode_or_cond):
    448                  raise ValueError, "Bad error condition"
    449          ErrorNode.__init__(self,xmlnode_or_cond,STREAM_ERROR_NS,copy=copy,parent=parent)
    450
    451 -    def get_message(self):
    452          """Get the message for the error.
    453
    454          :return: the error message.
    455          :returntype: `unicode`"""
    456          cond=self.get_condition()
    457          if not cond:
    458              self.upgrade()
    459              cond=self.get_condition()
    460              if not cond:
    461                  return None
    462          cond=cond.name
    463          if not stream_errors.has_key(cond):
    464              return None
    465          return stream_errors[cond][0]
    466
    467 -class StanzaErrorNode(ErrorNode):
    468      """Stanza error element."""
    469 -    def __init__(self,xmlnode_or_cond,error_type=None,copy=1,parent=None):
    470          """Initialize a StreamErrorNode object.
    471
    472          :Parameters:
    473              - `xmlnode_or_cond`: XML node to be wrapped into this object
    474                or the primary (defined by XMPP specification) error condition name.
    475              - `error_type`: type of the error, one of: 'cancel', 'continue',
    476                'modify', 'auth', 'wait'.
    477              - `copy`: When `True` then the XML node will be copied,
    478                otherwise it is only borrowed.
    479              - `parent`: Parent node for the XML node to be copied or created.
    480          :Types:
    481              - `xmlnode_or_cond`: `libxml2.xmlNode` or `unicode`
    482              - `error_type`: `unicode`
    483              - `copy`: `bool`
    484              - `parent`: `libxml2.xmlNode`"""
    485          if type(xmlnode_or_cond) is str:
    486              xmlnode_or_cond=unicode(xmlnode_or_cond,"utf-8")
    487          if type(xmlnode_or_cond) is unicode:
    488              if not stanza_errors.has_key(xmlnode_or_cond):
    489                  raise ValueError, "Bad error condition"
    490
    491          ErrorNode.__init__(self,xmlnode_or_cond,STANZA_ERROR_NS,copy=copy,parent=parent)
    492
    493          if type(xmlnode_or_cond) is unicode:
    494              if error_type is None:
    495                  error_type=stanza_errors[xmlnode_or_cond][1]
    496              self.xmlnode.setProp("type",to_utf8(error_type))
    497
    498 -    def get_type(self):
    499          """Get the error type.
    500
    501          :return: type of the error.
    502          :returntype: `unicode`"""
    503          if not self.xmlnode.hasProp("type"):
    504              self.upgrade()
    505          return from_utf8(self.xmlnode.prop("type"))
    506
    507 -    def upgrade(self):
    508          """Upgrade a legacy error element to the XMPP compliant one.
    509
    510          Use the error code provided to select the condition and the
    511          <error/> CDATA for the error text."""
    512          ErrorNode.upgrade(self)
    513          if self.xmlnode.hasProp("type"):
    514              return
    515
    516          cond=self.get_condition().name
    517          if stanza_errors.has_key(cond):
    518              typ=stanza_errors[cond][1]
    519              self.xmlnode.setProp("type",typ)
    520
    521 -    def get_message(self):
    522          """Get the message for the error.
    523
    524          :return: the error message.
    525          :returntype: `unicode`"""
    526          cond=self.get_condition()
    527          if not cond:
    528              self.upgrade()
    529              cond=self.get_condition()
    530              if not cond:
    531                  return None
    532          cond=cond.name
    533          if not stanza_errors.has_key(cond):
    534              return None
    535          return stanza_errors[cond][0]
    536
    537  # vi: sts=4 et sw=4
    538