Package pyxmpp :: Module error |
[hide private]

# Source Code for Module pyxmpp.error

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