class LegacyClientStream(ClientStream):
    """Handles Jabber (both XMPP and legacy protocol) client connection stream.

    Both client and server side of the connection is supported. This class handles
    client SASL and legacy authentication, authorisation and XMPP resource binding.
    """
    def __init__(self, jid, password = None, server = None, port = 5222,
            auth_methods = ("sasl:DIGEST-MD5", "digest"),
            tls_settings = None, keepalive = 0, owner = None):
        """Initialize a LegacyClientStream object.

        :Parameters:
            - `jid`: local JID.
            - `password`: user's password.
            - `server`: server to use. If not given then address will be derived form the JID.
            - `port`: port number to use. If not given then address will be derived form the JID.
            - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms
              in the list should be prefixed with "sasl:" string.
            - `tls_settings`: settings for StartTLS -- `TLSSettings` instance.
            - `keepalive`: keepalive output interval. 0 to disable.
            - `owner`: `Client`, `Component` or similar object "owning" this stream.
        :Types:
            - `jid`: `pyxmpp.JID`
            - `password`: `unicode`
            - `server`: `unicode`
            - `port`: `int`
            - `auth_methods`: sequence of `str`
            - `tls_settings`: `pyxmpp.TLSSettings`
            - `keepalive`: `int`
        """
        (self.authenticated, self.available_auth_methods, self.auth_stanza,
                self.peer_authenticated, self.auth_method_used,
                self.registration_callback, self.registration_form, self.__register) = (None,) * 8
        ClientStream.__init__(self, jid, password, server, port,
                            auth_methods, tls_settings, keepalive, owner)
        self.__logger=logging.getLogger("pyxmpp.jabber.LegacyClientStream")

    def _reset(self):
        """Reset the `LegacyClientStream` object state, making the object ready
        to handle new connections."""
        ClientStream._reset(self)
        self.available_auth_methods = None
        self.auth_stanza = None
        self.registration_callback = None

    def _post_connect(self):
        """Initialize authentication when the connection is established
        and we are the initiator."""
        if not self.initiator:
            if "plain" in self.auth_methods or "digest" in self.auth_methods:
                self.set_iq_get_handler("query","jabber:iq:auth",
                            self.auth_in_stage1)
                self.set_iq_set_handler("query","jabber:iq:auth",
                            self.auth_in_stage2)
        elif self.registration_callback:
            iq = Iq(stanza_type = "get")
            iq.set_content(Register())
            self.set_response_handlers(iq, self.registration_form_received, self.registration_error)
            self.send(iq)
            return
        ClientStream._post_connect(self)

    def _post_auth(self):
        """Unregister legacy authentication handlers after successfull
        authentication."""
        ClientStream._post_auth(self)
        if not self.initiator:
            self.unset_iq_get_handler("query","jabber:iq:auth")
            self.unset_iq_set_handler("query","jabber:iq:auth")

    def _try_auth(self):
        """Try to authenticate using the first one of allowed authentication
        methods left.

        [client only]"""
        if self.authenticated:
            self.__logger.debug("try_auth: already authenticated")
            return
        self.__logger.debug("trying auth: %r" % (self._auth_methods_left,))
        if not self._auth_methods_left:
            raise LegacyAuthenticationError,"No allowed authentication methods available"
        method=self._auth_methods_left[0]
        if method.startswith("sasl:"):
            return ClientStream._try_auth(self)
        elif method not in ("plain","digest"):
            self._auth_methods_left.pop(0)
            self.__logger.debug("Skipping unknown auth method: %s" % method)
            return self._try_auth()
        elif self.available_auth_methods is not None:
            if method in self.available_auth_methods:
                self._auth_methods_left.pop(0)
                self.auth_method_used=method
                if method=="digest":
                    self._digest_auth_stage2(self.auth_stanza)
                else:
                    self._plain_auth_stage2(self.auth_stanza)
                self.auth_stanza=None
                return
            else:
                self.__logger.debug("Skipping unavailable auth method: %s" % method)
        else:
            self._auth_stage1()

    def auth_in_stage1(self,stanza):
        """Handle the first stage (<iq type='get'/>) of legacy ("plain" or
        "digest") authentication.

        [server only]"""
        self.lock.acquire()
        try:
            if "plain" not in self.auth_methods and "digest" not in self.auth_methods:
                iq=stanza.make_error_response("not-allowed")
                self.send(iq)
                return

            iq=stanza.make_result_response()
            q=iq.new_query("jabber:iq:auth")
            q.newChild(None,"username",None)
            q.newChild(None,"resource",None)
            if "plain" in self.auth_methods:
                q.newChild(None,"password",None)
            if "digest" in self.auth_methods:
                q.newChild(None,"digest",None)
            self.send(iq)
            iq.free()
        finally:
            self.lock.release()

    def auth_in_stage2(self,stanza):
        """Handle the second stage (<iq type='set'/>) of legacy ("plain" or
        "digest") authentication.

        [server only]"""
        self.lock.acquire()
        try:
            if "plain" not in self.auth_methods and "digest" not in self.auth_methods:
                iq=stanza.make_error_response("not-allowed")
                self.send(iq)
                return

            username=stanza.xpath_eval("a:query/a:username",{"a":"jabber:iq:auth"})
            if username:
                username=from_utf8(username[0].getContent())
            resource=stanza.xpath_eval("a:query/a:resource",{"a":"jabber:iq:auth"})
            if resource:
                resource=from_utf8(resource[0].getContent())
            if not username or not resource:
                self.__logger.debug("No username or resource found in auth request")
                iq=stanza.make_error_response("bad-request")
                self.send(iq)
                return

            if stanza.xpath_eval("a:query/a:password",{"a":"jabber:iq:auth"}):
                if "plain" not in self.auth_methods:
                    iq=stanza.make_error_response("not-allowed")
                    self.send(iq)
                    return
                else:
                    return self._plain_auth_in_stage2(username,resource,stanza)
            if stanza.xpath_eval("a:query/a:digest",{"a":"jabber:iq:auth"}):
                if "plain" not in self.auth_methods:
                    iq=stanza.make_error_response("not-allowed")
                    self.send(iq)
                    return
                else:
                    return self._digest_auth_in_stage2(username,resource,stanza)
        finally:
            self.lock.release()

    def _auth_stage1(self):
        """Do the first stage (<iq type='get'/>) of legacy ("plain" or
        "digest") authentication.

        [client only]"""
        iq=Iq(stanza_type="get")
        q=iq.new_query("jabber:iq:auth")
        q.newTextChild(None,"username",to_utf8(self.my_jid.node))
        q.newTextChild(None,"resource",to_utf8(self.my_jid.resource))
        self.send(iq)
        self.set_response_handlers(iq,self.auth_stage2,self.auth_error,
                            self.auth_timeout,timeout=60)
        iq.free()

    def auth_timeout(self):
        """Handle legacy authentication timeout.

        [client only]"""
        self.lock.acquire()
        try:
            self.__logger.debug("Timeout while waiting for jabber:iq:auth result")
            if self._auth_methods_left:
                self._auth_methods_left.pop(0)
        finally:
            self.lock.release()

    def auth_error(self,stanza):
        """Handle legacy authentication error.

        [client only]"""
        self.lock.acquire()
        try:
            err=stanza.get_error()
            ae=err.xpath_eval("e:*",{"e":"jabber:iq:auth:error"})
            if ae:
                ae=ae[0].name
            else:
                ae=err.get_condition().name
            raise LegacyAuthenticationError,("Authentication error condition: %s"
                        % (ae,))
        finally:
            self.lock.release()

    def auth_stage2(self,stanza):
        """Handle the first stage authentication response (result of the <iq
        type="get"/>).

        [client only]"""
        self.lock.acquire()
        try:
            self.__logger.debug("Procesing auth response...")
            self.available_auth_methods=[]
            if (stanza.xpath_eval("a:query/a:digest",{"a":"jabber:iq:auth"}) and self.stream_id):
                self.available_auth_methods.append("digest")
            if (stanza.xpath_eval("a:query/a:password",{"a":"jabber:iq:auth"})):
                self.available_auth_methods.append("plain")
            self.auth_stanza=stanza.copy()
            self._try_auth()
        finally:
            self.lock.release()

    def _plain_auth_stage2(self, _unused):
        """Do the second stage (<iq type='set'/>) of legacy "plain"
        authentication.

        [client only]"""
        iq=Iq(stanza_type="set")
        q=iq.new_query("jabber:iq:auth")
        q.newTextChild(None,"username",to_utf8(self.my_jid.node))
        q.newTextChild(None,"resource",to_utf8(self.my_jid.resource))
        q.newTextChild(None,"password",to_utf8(self.password))
        self.send(iq)
        self.set_response_handlers(iq,self.auth_finish,self.auth_error)
        iq.free()

    def _plain_auth_in_stage2(self, username, _unused, stanza):
        """Handle the second stage (<iq type='set'/>) of legacy "plain"
        authentication.

        [server only]"""
        password=stanza.xpath_eval("a:query/a:password",{"a":"jabber:iq:auth"})
        if password:
            password=from_utf8(password[0].getContent())
        if not password:
            self.__logger.debug("No password found in plain auth request")
            iq=stanza.make_error_response("bad-request")
            self.send(iq)
            return

        if self.check_password(username,password):
            iq=stanza.make_result_response()
            self.send(iq)
            self.peer_authenticated=True
            self.auth_method_used="plain"
            self.state_change("authorized",self.peer)
            self._post_auth()
        else:
            self.__logger.debug("Plain auth failed")
            iq=stanza.make_error_response("bad-request")
            e=iq.get_error()
            e.add_custom_condition('jabber:iq:auth:error',"user-unauthorized")
            self.send(iq)

    def _digest_auth_stage2(self, _unused):
        """Do the second stage (<iq type='set'/>) of legacy "digest"
        authentication.

        [client only]"""
        iq=Iq(stanza_type="set")
        q=iq.new_query("jabber:iq:auth")
        q.newTextChild(None,"username",to_utf8(self.my_jid.node))
        q.newTextChild(None,"resource",to_utf8(self.my_jid.resource))

        digest = hashlib.sha1(to_utf8(self.stream_id)+to_utf8(self.password)).hexdigest()

        q.newTextChild(None,"digest",digest)
        self.send(iq)
        self.set_response_handlers(iq,self.auth_finish,self.auth_error)
        iq.free()

    def _digest_auth_in_stage2(self, username, _unused, stanza):
        """Handle the second stage (<iq type='set'/>) of legacy "digest"
        authentication.

        [server only]"""
        digest=stanza.xpath_eval("a:query/a:digest",{"a":"jabber:iq:auth"})
        if digest:
            digest=digest[0].getContent()
        if not digest:
            self.__logger.debug("No digest found in digest auth request")
            iq=stanza.make_error_response("bad-request")
            self.send(iq)
            return

        password,pwformat=self.get_password(username)
        if not password or pwformat!="plain":
            iq=stanza.make_error_response("bad-request")
            e=iq.get_error()
            e.add_custom_condition('jabber:iq:auth:error',"user-unauthorized")
            self.send(iq)
            return

        mydigest = hashlib.sha1(to_utf8(self.stream_id)+to_utf8(password)).hexdigest()

        if mydigest==digest:
            iq=stanza.make_result_response()
            self.send(iq)
            self.peer_authenticated=True
            self.auth_method_used="digest"
            self.state_change("authorized",self.peer)
            self._post_auth()
        else:
            self.__logger.debug("Digest auth failed: %r != %r" % (digest,mydigest))
            iq=stanza.make_error_response("bad-request")
            e=iq.get_error()
            e.add_custom_condition('jabber:iq:auth:error',"user-unauthorized")
            self.send(iq)

    def auth_finish(self, _unused):
        """Handle success of the legacy authentication."""
        self.lock.acquire()
        try:
            self.__logger.debug("Authenticated")
            self.authenticated=True
            self.state_change("authorized",self.my_jid)
            self._post_auth()
        finally:
            self.lock.release()

    def registration_error(self, stanza):
        """Handle in-band registration error.

        [client only]

        :Parameters:
            - `stanza`: the error stanza received or `None` on timeout.
        :Types:
            - `stanza`: `pyxmpp.stanza.Stanza`"""
        self.lock.acquire()
        try:
            err=stanza.get_error()
            ae=err.xpath_eval("e:*",{"e":"jabber:iq:auth:error"})
            if ae:
                ae=ae[0].name
            else:
                ae=err.get_condition().name
            raise RegistrationError,("Authentication error condition: %s" % (ae,))
        finally:
            self.lock.release()

    def registration_form_received(self, stanza):
        """Handle registration form received.

        [client only]

        Call self.registration_callback with the registration form received
        as the argument. Use the value returned by the callback will be a
        filled-in form.

        :Parameters:
            - `stanza`: the stanza received.
        :Types:
            - `stanza`: `pyxmpp.iq.Iq`"""
        self.lock.acquire()
        try:
            self.__register = Register(stanza.get_query())
            self.registration_callback(stanza, self.__register.get_form())
        finally:
            self.lock.release()

    def submit_registration_form(self, form):
        """Submit a registration form.

        [client only]

        :Parameters:
            - `form`: the filled-in form. When form is `None` or its type is
              "cancel" the registration is to be canceled.

        :Types:
            - `form`: `pyxmpp.jabber.dataforms.Form`"""
        self.lock.acquire()
        try:
            if form and form.type!="cancel":
                self.registration_form = form
                iq = Iq(stanza_type = "set")
                iq.set_content(self.__register.submit_form(form))
                self.set_response_handlers(iq, self.registration_success, self.registration_error)
                self.send(iq)
            else:
                self.__register = None
        finally:
            self.lock.release()

    def registration_success(self, stanza):
        """Handle registration success.

        [client only]

        Clean up registration stuff, change state to "registered" and initialize
        authentication.

        :Parameters:
            - `stanza`: the stanza received.
        :Types:
            - `stanza`: `pyxmpp.iq.Iq`"""
        _unused = stanza
        self.lock.acquire()
        try:
            self.state_change("registered", self.registration_form)
            if ('FORM_TYPE' in self.registration_form
                    and self.registration_form['FORM_TYPE'].value == 'jabber:iq:register'):
                if 'username' in self.registration_form:
                    self.my_jid = JID(self.registration_form['username'].value,
                            self.my_jid.domain, self.my_jid.resource)
                if 'password' in self.registration_form:
                    self.password = self.registration_form['password'].value
            self.registration_callback = None
            self._post_connect()
        finally:
            self.lock.release() DISCO_ITEMS_NS=DISCO_NS+"#items" DISCO_INFO_NS=DISCO_NS+"#info" class DiscoInfo(StanzaPayloadWrapperObject):
    """A disco#info response object.

    :Ivariables:
        - `node`: node name of the disco#info element (cached).
        - `identities`: identities in the disco#info object.
        - `features`: features in the disco#info object.
        - `xmlnode`: XML element listing the items.
    :Types:
        - `node`: `unicode`
        - `identities`: `tuple` of `DiscoIdentity`
        - `features`: `tuple` of `unicode`
        - `xmlnode`: `libxml2.xmlNode`
    """
    def __init__(self,xmlnode_or_node=None, parent=None, doc=None):
        """Initialize an `DiscoInfo` object.

        Wrap an existing disco#info XML element or create a new one.

        :Parameters:
            - `xmlnode_or_node`: XML node to be wrapped into `self` or an item
              node name.
            - `parent`: parent node for the `DiscoInfo` element.
            - `doc`: document for the `DiscoInfo` element.
        :Types:
            - `xmlnode_or_node`: `libxml2.xmlNode` or `unicode`
            - `parent`: `libxml2.xmlNode`
            - `doc`: `libxml2.xmlDoc`
            """
        self.xmlnode=None
        self.xpath_ctxt=None
        if not doc:
            doc=common_doc
        if not parent:
            parent=common_root
        if isinstance(xmlnode_or_node,libxml2.xmlNode):
            ns=xmlnode_or_node.ns()
            if ns.getContent() != DISCO_INFO_NS:
                raise ValueError, "Bad disco-info namespace"
            self.xmlnode=xmlnode_or_node.docCopyNode(doc,1)
            parent.addChild(self.xmlnode)
        else:
            self.xmlnode=parent.newChild(None,"query",None)
            self.ns=self.xmlnode.newNs(DISCO_INFO_NS,None)
            self.xmlnode.setNs(self.ns)
            self.set_node(xmlnode_or_node)

        self.xpath_ctxt=doc.xpathNewContext()
        self.xpath_ctxt.setContextNode(self.xmlnode)
        self.xpath_ctxt.xpathRegisterNs("d",DISCO_INFO_NS)

    def __del__(self):
        if self.xmlnode:
            self.xmlnode.unlinkNode()
            self.xmlnode.freeNode()
            self.xmlnode=None
        if self.xpath_ctxt:
            self.xpath_ctxt.xpathFreeContext()
            self.xpath_ctxt=None

    def get_node(self):
        """Get the node address of the `DiscoInfo` object.

        :return: the node name.
        :returntype: `unicode`"""

        node=self.xmlnode.prop("node")
        if not node:
            return None
        return node.decode("utf-8")

    def set_node(self,node):
        """Set the node of the disco#info element.

        :Parameters:
            - `node`: the new node or `None`.
        :Types:
            - `node`: `unicode`
        """
        if node is None:
            if self.xmlnode.hasProp("node"):
                self.xmlnode.unsetProp("node")
            return
        node = unicode(node)
        self.xmlnode.setProp("node", node.encode("utf-8"))

    node = property(get_node, set_node)

    def get_features(self):
        """Get the features contained in `self`.

        :return: the list of features.
        :returntype: `list` of `unicode`"""
        l = self.xpath_ctxt.xpathEval("d:feature")
        ret = []
        for f in l:
            if f.hasProp("var"):
                ret.append( f.prop("var").decode("utf-8") )
        return ret

    def set_features(self, features):
        """Set features in the disco#info object.

        All existing features are removed from `self`.

        :Parameters:
            - `features`: list of features.
        :Types:
            - `features`: sequence of `unicode`
        """
        for var in self.features:
            self.remove_feature(var)

        for var in features:
            self.add_feature(var)

    features = property(get_features, set_features)

    def has_feature(self,var):
        """Check if `self` contains the named feature.

        :Parameters:
            - `var`: the feature name.
        :Types:
            - `var`: `unicode`

        :return: `True` if the feature is found in `self`.
        :returntype: `bool`"""
        if not var:
            raise ValueError,"var is None"
        if '"' not in var:
            expr=u'd:feature[@var="%s"]' % (var,)
        elif "'" not in var:
            expr=u"d:feature[@var='%s']" % (var,)
        else:
            raise ValueError, "Invalid feature name"

        l=self.xpath_ctxt.xpathEval(to_utf8(expr))
        if l:
            return True
        else:
            return False

    def invalidate_features(self):
        """Clear cached feature list."""
        warnings.warn("DiscoInfo.invalidate_features() is deprecated and not needed any more.", DeprecationWarning, stacklevel=1)

    def add_feature(self,var):
        """Add a feature to `self`.

        :Parameters:
            - `var`: the feature name.
        :Types:
            - `var`: `unicode`"""
        if self.has_feature(var):
            return
        n=self.xmlnode.newChild(None, "feature", None)
        n.setProp("var", to_utf8(var))

    def remove_feature(self,var):
        """Remove a feature from `self`.

        :Parameters:
            - `var`: the feature name.
        :Types:
            - `var`: `unicode`"""
        if not var:
            raise ValueError,"var is None"
        if '"' not in var:
            expr='d:feature[@var="%s"]' % (var,)
        elif "'" not in var:
            expr="d:feature[@var='%s']" % (var,)
        else:
            raise ValueError, "Invalid feature name"

        l=self.xpath_ctxt.xpathEval(expr)
        if not l:
            return

        for f in l:
            f.unlinkNode()
            f.freeNode()

    def get_identities(self):
        """List the identity objects contained in `self`.

        :return: the list of identities.
        :returntype: `list` of `DiscoIdentity`"""
        ret=[]
        l=self.xpath_ctxt.xpathEval("d:identity")
        if l is not None:
            for i in l:
                ret.append(DiscoIdentity(self,i))
        return ret

    def set_identities(self,identities):
        """Set identities in the disco#info object.

        Remove all existing identities from `self`.

        :Parameters:
            - `identities`: list of identities or identity properties
              (jid,node,category,type,name).
        :Types:
            - `identities`: sequence of `DiscoIdentity` or sequence of sequences
        """
        for identity in self.identities:
            identity.remove()
        for identity in identities:
            try:
                self.add_identity(identity.name,identity.category,identity.type)
            except AttributeError:
                self.add_identity(*identity)

    identities = property(get_identities, set_identities)

    def identity_is(self,item_category,item_type=None):
        """Check if the item described by `self` belongs to the given category
        and type.

        :Parameters:
            - `item_category`: the category name.
            - `item_type`: the type name. If `None` then only the category is
              checked.
        :Types:
            - `item_category`: unicode
            - `item_type`: unicode

        :return: `True` if `self` contains at least one <identity/> object with
            given type and category.
        :returntype: `bool`"""
        if not item_category:
            raise ValueError, "bad category"
        if not item_type:
            type_expr=u""
        elif '"' not in item_type:
            type_expr=u' and @type="%s"' % (item_type,)
        elif "'" not in type:
            type_expr=u" and @type='%s'" % (item_type,)
        else:
            raise ValueError, "Invalid type name"
        if '"' not in item_category:
            expr=u'd:identity[@category="%s"%s]' % (item_category,type_expr)
        elif "'" not in item_category:
            expr=u"d:identity[@category='%s'%s]" % (item_category,type_expr)
        else:
            raise ValueError, "Invalid category name"

        l=self.xpath_ctxt.xpathEval(to_utf8(expr))
        if l:
            return True
        else:
            return False

    def invalidate_identities(self):
        """Clear cached identity list."""
        warnings.warn("DiscoInfo.invalidate_identities() is deprecated and not needed any more.", DeprecationWarning, stacklevel=1)

    def add_identity(self,item_name,item_category=None,item_type=None):
        """Add an identity to the `DiscoInfo` object.

        :Parameters:
            - `item_name`: name of the item.
            - `item_category`: category of the item.
            - `item_type`: type of the item.
        :Types:
            - `item_name`: `unicode`
            - `item_category`: `unicode`
            - `item_type`: `unicode`

        :returns: the identity created.
        :returntype: `DiscoIdentity`"""
        return DiscoIdentity(self,item_name,item_category,item_type) class DiscoItems(StanzaPayloadWrapperObject):
    """A disco#items response or publish-request object.

    :Ivariables:
        - `node`: node name of the disco#items element.
        - `items`: items in the disco#items element.
        - `xmlnode`: XML element listing the items.
    :Types:
        - `node`: `unicode`
        - `items`: `tuple` of `DiscoItem`
        - `xmlnode`: `libxml2.xmlNode`
    """
    def __init__(self,xmlnode_or_node=None):
        """Initialize an `DiscoItems` object.

        Wrap an existing disco#items XML element or create a new one.

        :Parameters:
            - `xmlnode_or_node`: XML node to be wrapped into `self` or an item
              node name.
        :Types:
            - `xmlnode_or_node`: `libxml2.xmlNode` or `unicode`"""
        self.xmlnode=None
        self.xpath_ctxt=None
        if isinstance(xmlnode_or_node,libxml2.xmlNode):
            ns=xmlnode_or_node.ns()
            if ns.getContent() != DISCO_ITEMS_NS:
                raise ValueError, "Bad disco-items namespace"
            self.xmlnode=xmlnode_or_node.docCopyNode(common_doc,1)
            common_root.addChild(self.xmlnode)
            self.ns=self.xmlnode.ns()
        else:
            self.xmlnode=common_root.newChild(None,"query",None)
            self.ns=self.xmlnode.newNs(DISCO_ITEMS_NS,None)
            self.xmlnode.setNs(self.ns)
            self.set_node(xmlnode_or_node)
        self.xpath_ctxt=common_doc.xpathNewContext()
        self.xpath_ctxt.setContextNode(self.xmlnode)
        self.xpath_ctxt.xpathRegisterNs("d",DISCO_ITEMS_NS)

    def __del__(self):
        if self.xmlnode:
            self.xmlnode.unlinkNode()
            self.xmlnode.freeNode()
            self.xmlnode=None
        if self.xpath_ctxt:
            self.xpath_ctxt.xpathFreeContext()
            self.xpath_ctxt=None

    def get_node(self):
        """Get the node address of the `DiscoItems` object.

        :return: the node name.
        :returntype: `unicode`"""
        node = self.xmlnode.prop("node")
        if not node:
            return None
        return node.decode("utf-8")

    def set_node(self, node):
        """Set the node of the disco#item element.

        :Parameters:
            - `node`: the new node or `None`.
        :Types:
            - `node`: `unicode`
        """
        if node is None:
            if self.xmlnode.hasProp("node"):
                self.xmlnode.unsetProp("node")
            return
        node = unicode(node)
        self.xmlnode.setProp("node", node.encode("utf-8"))

    node = property(get_node, set_node)

    def get_items(self):
        """Get the items contained in `self`.

        :return: the items contained.
        :returntype: `list` of `DiscoItem`"""
        ret=[]
        l=self.xpath_ctxt.xpathEval("d:item")
        if l is not None:
            for i in l:
                ret.append(DiscoItem(self, i))
        return ret

    def set_items(self, item_list):
        """Set items in the disco#items object.

        All previous items are removed.

        :Parameters:
            - `item_list`: list of items or item properties
              (jid,node,name,action).
        :Types:
            - `item_list`: sequence of `DiscoItem` or sequence of sequences
        """
        for item in self.items:
            item.remove()
        for item in item_list:
            try:
                self.add_item(item.jid,item.node,item.name,item.action)
            except AttributeError:
                self.add_item(*item)

    items = property(get_items, set_items, doc = "List of `DiscoItems`")

    def invalidate_items(self):
        """Clear cached item list."""
        warnings.warn("DiscoItems.invalidate_items() is deprecated and not needed any more.", DeprecationWarning, stacklevel=1)

    def add_item(self,jid,node=None,name=None,action=None):
        """Add a new item to the `DiscoItems` object.

        :Parameters:
            - `jid`: item JID.
            - `node`: item node name.
            - `name`: item name.
            - `action`: action for a "disco push".
        :Types:
            - `jid`: `pyxmpp.JID`
            - `node`: `unicode`
            - `name`: `unicode`
            - `action`: `unicode`

        :returns: the item created.
        :returntype: `DiscoItem`."""
        return DiscoItem(self,jid,node,name,action)

    def has_item(self,jid,node=None):
        """Check if `self` contains an item.

        :Parameters:
            - `jid`: JID of the item.
            - `node`: node name of the item.
        :Types:
            - `jid`: `JID`
            - `node`: `libxml2.xmlNode`

        :return: `True` if the item is found in `self`.
        :returntype: `bool`"""
        l=self.xpath_ctxt.xpathEval("d:item")
        if l is None:
            return False
        for it in l:
            di=DiscoItem(self,it)
            if di.jid==jid and di.node==node:
                return True
        return False class DiscoIdentity(StanzaPayloadWrapperObject):
    """An <identity/> element of disco#info reply.

    Identifies an item by its name, category and type.

    :Ivariables:
        - `disco`: the disco reply this is the part of.
        - `xmlnode`: XML element describing the identity.
    :Types:
        - `disco`: `DiscoInfo`
        - `xmlnode`: `libxml2.xmlNode`
    """
    def __init__(self, disco, xmlnode_or_name, item_category=None, item_type=None, replace=False):
        """Initialize an `DiscoIdentity` object.

        :Parameters:
            - `disco`: the disco#info reply `self` is a part of.
            - `xmlnode_or_name`: XML element describing the identity or the
              name of the item described.
            - `item_category`: category of the item described.
            - `item_type`: type of the item described.
            - `replace`: if `True` than all other <identity/> elements in
              `disco` will be removed.
        :Types:
            - `disco`: `DiscoItems`
            - `xmlnode_or_name`: `libxml2.xmlNode` or `unicode`
            - `item_category`: `unicode`
            - `item_type`: `unicode`
            - `replace`: `bool`
        """
        self.disco=disco
        if disco and replace:
            old=disco.xpath_ctxt.xpathEval("d:identity")
            if old:
                for n in old:
                    n.unlinkNode()
                    n.freeNode()
        if isinstance(xmlnode_or_name,libxml2.xmlNode):
            if disco is None:
                self.xmlnode=xmlnode_or_name.copyNode(1)
            else:
                self.xmlnode=xmlnode_or_name
        elif not item_category:
            raise ValueError,"DiscoInfo requires category"
        elif not item_type:
            raise ValueError,"DiscoInfo requires type"
        else:
            if disco:
                self.xmlnode=disco.xmlnode.newChild(None,"identity",None)
            else:
                self.xmlnode=common_root.newChild(None,"identity",None)
                ns=self.xmlnode.newNs(DISCO_INFO_NS,None)
                self.xmlnode.setNs(ns)
            self.set_name(xmlnode_or_name)
            self.set_category(item_category)
            self.set_type(item_type)
        self.xpath_ctxt=common_doc.xpathNewContext()
        self.xpath_ctxt.setContextNode(self.xmlnode)
        self.xpath_ctxt.xpathRegisterNs("d",DISCO_INFO_NS)

    def __del__(self):
        if self.disco is None:
            if self.xmlnode:
                self.xmlnode.unlinkNode()
                self.xmlnode.freeNode()
                self.xmlnode=None
        if self.xpath_ctxt:
            self.xpath_ctxt.xpathFreeContext()

    def __str__(self):
        return self.xmlnode.serialize()

    def remove(self):
        """Remove `self` from the containing `DiscoInfo` object."""
        if self.disco is None:
            return
        self.xmlnode.unlinkNode()
        oldns=self.xmlnode.ns()
        ns=self.xmlnode.newNs(oldns.getContent(),None)
        self.xmlnode.replaceNs(oldns,ns)
        common_root.addChild(self.xmlnode())
        self.disco=None

    def get_name(self):
        """Get the name of the item.

        :return: the name of the item or `None`.
        :returntype: `unicode`"""
        var = self.xmlnode.prop("name")
        if not var:
            var = ""
        return var.decode("utf-8")

    def set_name(self,name):
        """Set the name of the item.

        :Parameters:
            - `name`: the new name or `None`.
        :Types:
            - `name`: `unicode` """
        if not name:
            raise ValueError, "name is required in DiscoIdentity"
        name = unicode(name)
        self.xmlnode.setProp("name", name.encode("utf-8"))

    name = property(get_name, set_name)

    def get_category(self):
        """Get the category of the item.

        :return: the category of the item.
        :returntype: `unicode`"""
        var = self.xmlnode.prop("category")
        if not var:
            var = "?"
        return var.decode("utf-8")

    def set_category(self, category):
        """Set the category of the item.

        :Parameters:
            - `category`: the new category.
        :Types:
            - `category`: `unicode` """
        if not category:
            raise ValueError, "Category is required in DiscoIdentity"
        category = unicode(category)
        self.xmlnode.setProp("category", category.encode("utf-8"))

    category = property(get_category, set_category)

    def get_type(self):
        """Get the type of the item.

        :return: the type of the item.
        :returntype: `unicode`"""
        item_type = self.xmlnode.prop("type")
        if not item_type:
            item_type = "?"
        return item_type.decode("utf-8")

    def set_type(self, item_type):
        """Set the type of the item.

        :Parameters:
            - `item_type`: the new type.
        :Types:
            - `item_type`: `unicode` """
        if not item_type:
            raise ValueError,"Type is required in DiscoIdentity"
        item_type = unicode(item_type)
        self.xmlnode.setProp("type", item_type.encode("utf-8"))

    type = property(get_type, set_type) DISCO_NS="http://jabber.org/protocol/disco"
DISCO_ITEMS_NS=DISCO_NS+"#items"
DISCO_INFO_NS=DISCO_NS+"#info"
class DiscoItem(StanzaPayloadWrapperObject):
class DiscoIdentity(StanzaPayloadWrapperObject):
class DiscoItems(StanzaPayloadWrapperObject):
class DiscoInfo(StanzaPayloadWrapperObject):
class DiscoCacheFetcherBase(cache.CacheFetcher):
    class DiscoInfoCacheFetcher(DiscoCacheFetcherBase):
    class DiscoItemsCacheFetcher(DiscoCacheFetcherBase):
    def __init__(self,disco,xmlnode_or_jid,node=None,name=None,action=None):
    def __del__(self):
    def __str__(self):
    def remove(self):
    def get_name(self):
    def set_name(self, name):
    def get_node(self):
    def set_node(self,node):
    def get_action(self):
    def set_action(self,action):
    def get_jid(self):
    def set_jid(self,jid):
    def __init__(self, disco, xmlnode_or_name, item_category=None, item_type=None, replace=False):
    def __del__(self):
    def __str__(self):
    def remove(self):
    def get_name(self):
    def set_name(self,name):
    def get_category(self):
    def set_category(self, category):
    def get_type(self):
    def set_type(self, item_type):
    def __init__(self,xmlnode_or_node=None):
    def __del__(self):
    def get_node(self):
    def set_node(self, node):
    def get_items(self):
    def set_items(self, item_list):
    def invalidate_items(self):
    def add_item(self,jid,node=None,name=None,action=None):
    def has_item(self,jid,node=None):
    def __init__(self,xmlnode_or_node=None, parent=None, doc=None):
    def __del__(self):
    def get_node(self):
    def set_node(self,node):
    def get_features(self):
    def set_features(self, features):
    def has_feature(self,var):
    def invalidate_features(self):
    def add_feature(self,var):
    def remove_feature(self,var):
    def get_identities(self):
    def set_identities(self,identities):
    def identity_is(self,item_category,item_type=None):
    def invalidate_identities(self):
    def add_identity(self,item_name,item_category=None,item_type=None):
    def fetch(self):
    def __response(self,stanza):
    def __error(self,stanza):
    def __timeout(self,stanza):
def register_disco_cache_fetchers(cache_suite,stream): class Client:
    """Base class for an XMPP-IM client.

    This class does not provide any JSF extensions to the XMPP protocol,
    including legacy authentication methods.

    :Ivariables:
        - `jid`: configured JID of the client (current actual JID
          is avialable as `self.stream.jid`).
        - `password`: authentication password.
        - `server`: server to use if non-standard and not discoverable
          by SRV lookups.
        - `port`: port number on the server to use if non-standard and not
          discoverable by SRV lookups.
        - `auth_methods`: methods allowed for stream authentication. SASL
          mechanism names should be preceded with "sasl:" prefix.
        - `keepalive`: keepalive interval for the stream or 0 when keepalive is
          disabled.
        - `stream`: current stream when the client is connected,
          `None` otherwise.
        - `roster`: user's roster or `None` if the roster is not yet retrieved.
        - `session_established`: `True` when an IM session is established.
        - `lock`: lock for synchronizing `Client` attributes access.
        - `state_changed`: condition notified the the object state changes
          (stream becomes connected, session established etc.).
        - `interface_providers`: list of object providing interfaces that
          could be used by the Client object. Initialized to [`self`] by
          the constructor if not set earlier. Put objects providing 
          `IPresenceHandlersProvider`, `IMessageHandlersProvider`,
          `IIqHandlersProvider` or `IStanzaHandlersProvider` into this list.
    :Types:
        - `jid`: `pyxmpp.JID`
        - `password`: `unicode`
        - `server`: `unicode`
        - `port`: `int`
        - `auth_methods`: `list` of `str`
        - `keepalive`: `int`
        - `stream`: `pyxmpp.ClientStream`
        - `roster`: `pyxmpp.Roster`
        - `session_established`: `bool`
        - `lock`: `threading.RLock`
        - `state_changed`: `threading.Condition`
        - `interface_providers`: `list`
    """
    def __init__(self,jid=None,password=None,server=None,port=5222,
            auth_methods=("sasl:DIGEST-MD5",),
            tls_settings=None,keepalive=0):
        """Initialize a Client object.

        :Parameters:
            - `jid`: user full JID for the connection.
            - `password`: user password.
            - `server`: server to use. If not given then address will be derived form the JID.
            - `port`: port number to use. If not given then address will be derived form the JID.
            - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms
              in the list should be prefixed with "sasl:" string.
            - `tls_settings`: settings for StartTLS -- `TLSSettings` instance.
            - `keepalive`: keepalive output interval. 0 to disable.
        :Types:
            - `jid`: `pyxmpp.JID`
            - `password`: `unicode`
            - `server`: `unicode`
            - `port`: `int`
            - `auth_methods`: sequence of `str`
            - `tls_settings`: `pyxmpp.TLSSettings`
            - `keepalive`: `int`
        """
        self.jid=jid
        self.password=password
        self.server=server
        self.port=port
        self.auth_methods=list(auth_methods)
        self.tls_settings=tls_settings
        self.keepalive=keepalive
        self.stream=None
        self.lock=threading.RLock()
        self.state_changed=threading.Condition(self.lock)
        self.session_established=False
        self.roster=None
        self.stream_class=ClientStream
        if not hasattr(self, "interface_providers"):
            self.interface_providers = [self]
        self.__logger=logging.getLogger("pyxmpp.Client")

# public methods

    def connect(self, register = False):
        """Connect to the server and set up the stream.

        Set `self.stream` and notify `self.state_changed` when connection
        succeeds."""
        if not self.jid:
            raise ClientError, "Cannot connect: no or bad JID given"
        self.lock.acquire()
        try:
            stream = self.stream
            self.stream = None
            if stream:
                stream.close()

            self.__logger.debug("Creating client stream: %r, auth_methods=%r"
                    % (self.stream_class, self.auth_methods))
            stream=self.stream_class(jid = self.jid,
                    password = self.password,
                    server = self.server,
                    port = self.port,
                    auth_methods = self.auth_methods,
                    tls_settings = self.tls_settings,
                    keepalive = self.keepalive,
                    owner = self)
            stream.process_stream_error = self.stream_error
            self.stream_created(stream)
            stream.state_change = self.__stream_state_change
            stream.connect()
            self.stream = stream
            self.state_changed.notify()
            self.state_changed.release()
        except:
            self.stream = None
            self.state_changed.release()
            raise

    def get_stream(self):
        """Get the connected stream object.

        :return: stream object or `None` if the client is not connected.
        :returntype: `pyxmpp.ClientStream`"""
        self.lock.acquire()
        stream=self.stream
        self.lock.release()
        return stream

    def disconnect(self):
        """Disconnect from the server."""
        stream=self.get_stream()
        if stream:
            stream.disconnect()

    def request_session(self):
        """Request an IM session."""
        stream=self.get_stream()
        if not stream.version:
            need_session=False
        elif not stream.features:
            need_session=False
        else:
            ctxt = stream.doc_in.xpathNewContext()
            ctxt.setContextNode(stream.features)
            ctxt.xpathRegisterNs("sess","urn:ietf:params:xml:ns:xmpp-session")
            # jabberd2 hack
            ctxt.xpathRegisterNs("jsess","http://jabberd.jabberstudio.org/ns/session/1.0")
            sess_n=None
            try:
                sess_n=ctxt.xpathEval("sess:session or jsess:session")
            finally:
                ctxt.xpathFreeContext()
            if sess_n:
                need_session=True
            else:
                need_session=False

        if not need_session:
            self.state_changed.acquire()
            self.session_established=1
            self.state_changed.notify()
            self.state_changed.release()
            self._session_started()
        else:
            iq=Iq(stanza_type="set")
            iq.new_query("urn:ietf:params:xml:ns:xmpp-session","session")
            stream.set_response_handlers(iq,
                self.__session_result,self.__session_error,self.__session_timeout)
            stream.send(iq)

    def request_roster(self):
        """Request the user's roster."""
        stream=self.get_stream()
        iq=Iq(stanza_type="get")
        iq.new_query("jabber:iq:roster")
        stream.set_response_handlers(iq,
            self.__roster_result,self.__roster_error,self.__roster_timeout)
        stream.set_iq_set_handler("query","jabber:iq:roster",self.__roster_push)
        stream.send(iq)

    def get_socket(self):
        """Get the socket object of the active connection.

        :return: socket used by the stream.
        :returntype: `socket.socket`"""
        return self.stream.socket

    def loop(self,timeout=1):
        """Simple "main loop" for the client.

        By default just call the `pyxmpp.Stream.loop_iter` method of
        `self.stream`, which handles stream input and `self.idle` for some
        "housekeeping" work until the stream is closed.

        This usually will be replaced by something more sophisticated. E.g.
        handling of other input sources."""
        while 1:
            stream=self.get_stream()
            if not stream:
                break
            act=stream.loop_iter(timeout)
            if not act:
                self.idle()

# private methods

    def __session_timeout(self):
        """Process session request time out.

        :raise FatalClientError:"""
        raise FatalClientError("Timeout while tryin to establish a session")

    def __session_error(self,iq):
        """Process session request failure.

        :Parameters:
            - `iq`: IQ error stanza received as result of the session request.
        :Types:
            - `iq`: `pyxmpp.Iq`

        :raise FatalClientError:"""
        err=iq.get_error()
        msg=err.get_message()
        raise FatalClientError("Failed to establish a session: "+msg)

    def __session_result(self, _unused):
        """Process session request success.

        :Parameters:
            - `_unused`: IQ result stanza received in reply to the session request.
        :Types:
            - `_unused`: `pyxmpp.Iq`"""
        self.state_changed.acquire()
        self.session_established=True
        self.state_changed.notify()
        self.state_changed.release()
        self._session_started()

    def _session_started(self):
        """Called when session is started.
        
        Activates objects from `self.interface_provides` by installing
        their stanza handlers, etc."""
        for ob in self.interface_providers:
            if IPresenceHandlersProvider.providedBy(ob):
                for handler_data in ob.get_presence_handlers():
                    self.stream.set_presence_handler(*handler_data)
            if IMessageHandlersProvider.providedBy(ob):
                for handler_data in ob.get_message_handlers():
                    self.stream.set_message_handler(*handler_data)
            if IIqHandlersProvider.providedBy(ob):
                for handler_data in ob.get_iq_get_handlers():
                    self.stream.set_iq_get_handler(*handler_data)
                for handler_data in ob.get_iq_set_handlers():
                    self.stream.set_iq_set_handler(*handler_data)
        self.session_started()

    def __roster_timeout(self):
        """Process roster request time out.

        :raise ClientError:"""
        raise ClientError("Timeout while tryin to retrieve roster")

    def __roster_error(self,iq):
        """Process roster request failure.

        :Parameters:
            - `iq`: IQ error stanza received as result of the roster request.
        :Types:
            - `iq`: `pyxmpp.Iq`

        :raise ClientError:"""
        err=iq.get_error()
        msg=err.get_message()
        raise ClientError("Roster retrieval failed: "+msg)

    def __roster_result(self,iq):
        """Process roster request success.

        :Parameters:
            - `iq`: IQ result stanza received in reply to the roster request.
        :Types:
            - `iq`: `pyxmpp.Iq`"""
        q=iq.get_query()
        if q:
            self.state_changed.acquire()
            self.roster=Roster(q)
            self.state_changed.notify()
            self.state_changed.release()
            self.roster_updated()
        else:
            raise ClientError("Roster retrieval failed")

    def __roster_push(self,iq):
        """Process a "roster push" (change notification) received.

        :Parameters:
            - `iq`: IQ result stanza received.
        :Types:
            - `iq`: `pyxmpp.Iq`"""
        fr=iq.get_from()
        if fr and fr != self.jid and fr != self.jid.bare():
            resp=iq.make_error_response("forbidden")
            self.stream.send(resp)
            self.__logger.warning("Got roster update from wrong source")
            return
        if not self.roster:
            raise ClientError("Roster update, but no roster")
        q=iq.get_query()
        item=self.roster.update(q)
        if item:
            self.roster_updated(item)
        resp=iq.make_result_response()
        self.stream.send(resp)

    def __stream_state_change(self,state,arg):
        """Handle stream state changes.

        Call apopriate methods of self.

        :Parameters:
            - `state`: the new state.
            - `arg`: state change argument.
        :Types:
            - `state`: `str`"""
        self.stream_state_changed(state,arg)
        if state=="fully connected":
            self.connected()
        elif state=="authorized":
            self.authorized()
        elif state=="disconnected":
            self.state_changed.acquire()
            try:
                if self.stream:
                    self.stream.close()
                self.stream_closed(self.stream)
                self.stream=None
                self.state_changed.notify()
            finally:
                self.state_changed.release()
            self.disconnected()

# Method to override
    def idle(self):
        """Do some "housekeeping" work like cache expiration or timeout
        handling. Should be called periodically from the application main
        loop. May be overriden in derived classes."""
        stream=self.get_stream()
        if stream:
            stream.idle()

    def stream_created(self,stream):
        """Handle stream creation event. May be overriden in derived classes.
        This one does nothing.

        :Parameters:
            - `stream`: the new stream.
        :Types:
            - `stream`: `pyxmpp.ClientStream`"""
        pass

    def stream_closed(self,stream):
        """Handle stream closure event. May be overriden in derived classes.
        This one does nothing.

        :Parameters:
            - `stream`: the new stream.
        :Types:
            - `stream`: `pyxmpp.ClientStream`"""
        pass

    def session_started(self):
        """Handle session started event. May be overriden in derived classes.
        This one requests the user's roster and sends the initial presence."""
        self.request_roster()
        p=Presence()
        self.stream.send(p)

    def stream_error(self,err):
        """Handle stream error received. May be overriden in derived classes.
        This one passes an error messages to logging facilities.

        :Parameters:
            - `err`: the error element received.
        :Types:
            - `err`: `pyxmpp.error.StreamErrorNode`"""
        self.__logger.error("Stream error: condition: %s %r"
                % (err.get_condition().name,err.serialize()))

    def roster_updated(self,item=None):
        """Handle roster update event. May be overriden in derived classes.
        This one does nothing.

        :Parameters:
            - `item`: the roster item changed or `None` if whole roster was
              received.
        :Types:
            - `item`: `pyxmpp.RosterItem`"""
        pass

    def stream_state_changed(self,state,arg):
        """Handle any stream state change. May be overriden in derived classes.
        This one does nothing.

        :Parameters:
            - `state`: the new state.
            - `arg`: state change argument.
        :Types:
            - `state`: `str`"""
        pass

    def connected(self):
        """Handle "connected" event. May be overriden in derived classes.
        This one does nothing."""
        pass

    def authenticated(self):
        """Handle "authenticated" event. May be overriden in derived classes.
        This one does nothing."""
        pass

    def authorized(self):
        """Handle "authorized" event. May be overriden in derived classes.
        This one requests an IM session."""
        self.request_session()

    def disconnected(self):
        """Handle "disconnected" event. May be overriden in derived classes.
        This one does nothing."""
        pass class Stanza:
    """Base class for all XMPP stanzas.

    :Ivariables:
        - `xmlnode`: stanza XML node.
        - `_error`: `pyxmpp.error.StanzaErrorNode` describing the error associated with
          the stanza of type "error".
        - `stream`: stream on which the stanza was received or `None`. May be
          used to send replies or get some session-related parameters.
    :Types:
        - `xmlnode`: `libxml2.xmlNode`
        - `_error`: `pyxmpp.error.StanzaErrorNode`"""
    stanza_type="Unknown"

    def __init__(self, name_or_xmlnode, from_jid=None, to_jid=None,
            stanza_type=None, stanza_id=None, error=None, error_cond=None,
            stream = None):
        """Initialize a Stanza object.

        :Parameters:
            - `name_or_xmlnode`: XML node to be wrapped into the Stanza object
              or other Presence object to be copied. If not given then new
              presence stanza is created using following parameters.
            - `from_jid`: sender JID.
            - `to_jid`: recipient JID.
            - `stanza_type`: staza type: one of: "get", "set", "result" or "error".
            - `stanza_id`: stanza id -- value of stanza's "id" attribute. If
              not given, then unique for the session value is generated.
            - `error`: error object. Ignored if `stanza_type` is not "error".
            - `error_cond`: error condition name. Ignored if `stanza_type` is not
              "error" or `error` is not None.
        :Types:
            - `name_or_xmlnode`: `unicode` or `libxml2.xmlNode` or `Stanza`
            - `from_jid`: `JID`
            - `to_jid`: `JID`
            - `stanza_type`: `unicode`
            - `stanza_id`: `unicode`
            - `error`: `pyxmpp.error.StanzaErrorNode`
            - `error_cond`: `unicode`"""
        self._error=None
        self.xmlnode=None
        if isinstance(name_or_xmlnode,Stanza):
            self.xmlnode=name_or_xmlnode.xmlnode.docCopyNode(common_doc, True)
            common_doc.addChild(self.xmlnode)
            self.xmlnode.reconciliateNs(common_doc)
        elif isinstance(name_or_xmlnode,libxml2.xmlNode):
            self.xmlnode=name_or_xmlnode.docCopyNode(common_doc,1)
            common_doc.addChild(self.xmlnode)
            try:
                ns = self.xmlnode.ns()
            except libxml2.treeError:
                ns = None
            if not ns or not ns.name:
                xmlextra.replace_ns(self.xmlnode, ns, common_ns)
            self.xmlnode.reconciliateNs(common_doc)
        else:
            self.xmlnode=common_doc.newChild(common_ns,name_or_xmlnode,None)

        if from_jid is not None:
            if not isinstance(from_jid,JID):
                from_jid=JID(from_jid)
            self.xmlnode.setProp("from",from_jid.as_utf8())

        if to_jid is not None:
            if not isinstance(to_jid,JID):
                to_jid=JID(to_jid)
            self.xmlnode.setProp("to",to_jid.as_utf8())

        if stanza_type:
            self.xmlnode.setProp("type",stanza_type)

        if stanza_id:
            self.xmlnode.setProp("id",stanza_id)

        if self.get_type()=="error":
            from pyxmpp.error import StanzaErrorNode
            if error:
                self._error=StanzaErrorNode(error,parent=self.xmlnode,copy=1)
            elif error_cond:
                self._error=StanzaErrorNode(error_cond,parent=self.xmlnode)
        self.stream = stream

    def __del__(self):
        if self.xmlnode:
            self.free()

    def free(self):
        """Free the node associated with this `Stanza` object."""
        if self._error:
            self._error.free_borrowed()
        self.xmlnode.unlinkNode()
        self.xmlnode.freeNode()
        self.xmlnode=None

    def copy(self):
        """Create a deep copy of the stanza.

        :returntype: `Stanza`"""
        return Stanza(self)

    def serialize(self):
        """Serialize the stanza into an UTF-8 encoded XML string.

        :return: serialized stanza.
        :returntype: `str`"""
        return self.xmlnode.serialize(encoding="utf-8")

    def get_node(self):
        """Return the XML node wrapped into `self`.

        :returntype: `libxml2.xmlNode`"""
        return self.xmlnode

    def get_from(self):
        """Get "from" attribute of the stanza.

        :return: value of the "from" attribute (sender JID) or None.
        :returntype: `JID`"""
        if self.xmlnode.hasProp("from"):
            try:
                return JID(from_utf8(self.xmlnode.prop("from")))
            except JIDError:
                raise JIDMalformedProtocolError, "Bad JID in the 'from' attribute"
        else:
            return None

    get_from_jid=get_from

    def get_to(self):
        """Get "to" attribute of the stanza.

        :return: value of the "to" attribute (recipient JID) or None.
        :returntype: `JID`"""
        if self.xmlnode.hasProp("to"):
            try:
                return JID(from_utf8(self.xmlnode.prop("to")))
            except JIDError:
                raise JIDMalformedProtocolError, "Bad JID in the 'to' attribute"
        else:
            return None

    get_to_jid=get_to

    def get_type(self):
        """Get "type" attribute of the stanza.

        :return: value of the "type" attribute (stanza type) or None.
        :returntype: `unicode`"""
        if self.xmlnode.hasProp("type"):
            return from_utf8(self.xmlnode.prop("type"))
        else:
            return None

    get_stanza_type=get_type

    def get_id(self):
        """Get "id" attribute of the stanza.

        :return: value of the "id" attribute (stanza identifier) or None.
        :returntype: `unicode`"""
        if self.xmlnode.hasProp("id"):
            return from_utf8(self.xmlnode.prop("id"))
        else:
            return None

    get_stanza_id=get_id

    def get_error(self):
        """Get stanza error information.

        :return: object describing the error.
        :returntype: `pyxmpp.error.StanzaErrorNode`"""
        if self._error:
            return self._error
        n=self.xpath_eval(u"ns:error")
        if not n:
            raise ProtocolError, (None, "This stanza contains no error: %r" % (self.serialize(),))
        from pyxmpp.error import StanzaErrorNode
        self._error=StanzaErrorNode(n[0],copy=0)
        return self._error

    def set_from(self,from_jid):
        """Set "from" attribute of the stanza.

        :Parameters:
            - `from_jid`: new value of the "from" attribute (sender JID).
        :Types:
            - `from_jid`: `JID`"""
        if from_jid:
            return self.xmlnode.setProp("from", JID(from_jid).as_utf8())
        else:
            return self.xmlnode.unsetProp("from")

    def set_to(self,to_jid):
        """Set "to" attribute of the stanza.

        :Parameters:
            - `to_jid`: new value of the "to" attribute (recipient JID).
        :Types:
            - `to_jid`: `JID`"""
        if to_jid:
            return self.xmlnode.setProp("to", JID(to_jid).as_utf8())
        else:
            return self.xmlnode.unsetProp("to")

    def set_type(self,stanza_type):
        """Set "type" attribute of the stanza.

        :Parameters:
            - `stanza_type`: new value of the "type" attribute (stanza type).
        :Types:
            - `stanza_type`: `unicode`"""
        if stanza_type:
            return self.xmlnode.setProp("type",to_utf8(stanza_type))
        else:
            return self.xmlnode.unsetProp("type")

    def set_id(self,stanza_id):
        """Set "id" attribute of the stanza.

        :Parameters:
            - `stanza_id`: new value of the "id" attribute (stanza identifier).
        :Types:
            - `stanza_id`: `unicode`"""
        if stanza_id:
            return self.xmlnode.setProp("id",to_utf8(stanza_id))
        else:
            return self.xmlnode.unsetProp("id")

    def set_content(self,content):
        """Set stanza content to an XML node.

        :Parameters:
            - `content`: XML node to be included in the stanza.
        :Types:
            - `content`: `libxml2.xmlNode` or unicode, or UTF-8 `str`
        """
        while self.xmlnode.children:
            self.xmlnode.children.unlinkNode()
        if hasattr(content,"as_xml"):
            content.as_xml(parent=self.xmlnode,doc=common_doc)
        elif isinstance(content,libxml2.xmlNode):
            self.xmlnode.addChild(content.docCopyNode(common_doc,1))
        elif isinstance(content,unicode):
            self.xmlnode.setContent(to_utf8(content))
        else:
            self.xmlnode.setContent(content)

    def add_content(self,content):
        """Add an XML node to the stanza's payload.

        :Parameters:
            - `content`: XML node to be added to the payload.
        :Types:
            - `content`: `libxml2.xmlNode`, UTF-8 `str` or unicode, or
               an object with "as_xml()" method.
        """
        if hasattr(content, "as_xml"):
            content.as_xml(parent = self.xmlnode, doc = common_doc)
        elif isinstance(content,libxml2.xmlNode):
            self.xmlnode.addChild(content.docCopyNode(common_doc,1))
        elif isinstance(content,unicode):
            self.xmlnode.addContent(to_utf8(content))
        else:
            self.xmlnode.addContent(content)

    def set_new_content(self,ns_uri,name):
        """Set stanza payload to a new XML element.

        :Parameters:
            - `ns_uri`: XML namespace URI of the element.
            - `name`: element name.
        :Types:
            - `ns_uri`: `str`
            - `name`: `str` or `unicode`
        """
        while self.xmlnode.children:
            self.xmlnode.children.unlinkNode()
        return self.add_new_content(ns_uri,name)

    def add_new_content(self,ns_uri,name):
        """Add a new XML element to the stanza payload.

        :Parameters:
            - `ns_uri`: XML namespace URI of the element.
            - `name`: element name.
        :Types:
            - `ns_uri`: `str`
            - `name`: `str` or `unicode`
        """
        c=self.xmlnode.newChild(None,to_utf8(name),None)
        if ns_uri:
            ns=c.newNs(ns_uri,None)
            c.setNs(ns)
        return c

    def xpath_eval(self,expr,namespaces=None):
        """Evaluate an XPath expression on the stanza XML node.

        The expression will be evaluated in context where the common namespace
        (the one used for stanza elements, mapped to 'jabber:client',
        'jabber:server', etc.) is bound to prefix "ns" and other namespaces are
        bound accordingly to the `namespaces` list.

        :Parameters:
            - `expr`: XPath expression.
            - `namespaces`: mapping from namespace prefixes to URIs.
        :Types:
            - `expr`: `unicode`
            - `namespaces`: `dict` or other mapping
        """
        ctxt = common_doc.xpathNewContext()
        ctxt.setContextNode(self.xmlnode)
        ctxt.xpathRegisterNs("ns",COMMON_NS)
        if namespaces:
            for prefix,uri in namespaces.items():
                ctxt.xpathRegisterNs(unicode(prefix),uri)
        ret=ctxt.xpathEval(unicode(expr))
        ctxt.xpathFreeContext()
        return ret

    def __eq__(self,other):
        if not isinstance(other,Stanza):
            return False
        return self.xmlnode.serialize()==other.xmlnode.serialize()

    def __ne__(self,other):
        if not isinstance(other,Stanza):
            return True
        return self.xmlnode.serialize()!=other.xmlnode.serialize() class CacheSuite:
    """Caching proxy for object retrieval and caching.

    Object factories for other classes are registered in the
    `Cache` object and used to e.g. retrieve requested objects from network.
    They are called only when the requested object is not in the cache
    or is not fresh enough.

    Objects are addressed using their class and a class dependant address.
    Eg. `pyxmpp.jabber.disco.DiscoInfo` objects are addressed using
    (`pyxmpp.jabber.disco.DiscoInfo`,(jid, node)) tuple.

    Additionaly a state (freshness level) name may be provided when requesting
    an object. When the cached item state is "less fresh" then requested, then
    new object will be retrieved.

    Following states are defined:

      - 'new': always a new object should be retrieved.
      - 'fresh': a fresh object (not older than freshness time)
      - 'old': object not fresh, but most probably still valid.
      - 'stale': object known to be expired.

    :Ivariables:
        - `default_freshness_period`: default freshness period (in seconds).
        - `default_expiration_period`: default expiration period (in seconds).
        - `default_purge_period`: default purge period (in seconds). When
          0 then items are never purged because of their age.
        - `max_items`: maximum number of obejects of one class to store.
        - `_caches`: dictionary of per-class caches.
        - `_lock`: lock for thread safety.
    :Types:
        - `default_freshness_period`: timedelta
        - `default_expiration_period`: timedelta
        - `default_purge_period`: timedelta
        - `max_items`: `int`
        - `_caches`: `dict` of (`classobj`, addr) -> `Cache`
        - `_lock`: `threading.RLock`
    """
    def __init__(self, max_items, default_freshness_period = _hour,
            default_expiration_period = 12*_hour, default_purge_period = 24*_hour):
        """Initialize a `Cache` object.

            :Parameters:
                - `default_freshness_period`: default freshness period (in seconds).
                - `default_expiration_period`: default expiration period (in seconds).
                - `default_purge_period`: default purge period (in seconds). When
                  0 then items are never purged because of their age.
                - `max_items`: maximum number of items to store.
            :Types:
                - `default_freshness_period`: number
                - `default_expiration_period`: number
                - `default_purge_period`: number
                - `max_items`: number
        """
        self.default_freshness_period = default_freshness_period
        self.default_expiration_period = default_expiration_period
        self.default_purge_period = default_purge_period
        self.max_items = max_items
        self._caches = {}
        self._lock = threading.RLock()

    def request_object(self, object_class, address, state, object_handler,
            error_handler = None, timeout_handler = None,
            backup_state = None, timeout = None,
            freshness_period = None, expiration_period = None, purge_period = None):
        """Request an object of given class, with given address and state not
        worse than `state`. The object will be taken from cache if available,
        and created/fetched otherwise. The request is asynchronous -- this
        metod doesn't return the object directly, but the `object_handler` is
        called as soon as the object is available (this may be before
        `request_object` returns and may happen in other thread). On error the
        `error_handler` will be called, and on timeout -- the
        `timeout_handler`.

        :Parameters:
            - `object_class`: class (type) of the object requested.
            - `address`: address of the object requested.
            - `state`: the worst acceptable object state. When 'new' then always
              a new object will be created/fetched. 'stale' will select any
              item available in cache.
            - `object_handler`: function to be called when object is available.
              It will be called with the following arguments: address, object
              and its state.
            - `error_handler`: function to be called on object retrieval error.
              It will be called with two arguments: requested address and
              additional error information (fetcher-specific, may be
              StanzaError for XMPP objects).  If not given, then the object
              handler will be called with object set to `None` and state
              "error".
            - `timeout_handler`: function to be called on object retrieval
              timeout.  It will be called with only one argument: the requested
              address. If not given, then the `error_handler` will be called
              instead, with error details set to `None`.
            - `backup_state`: when set and object in state `state` is not
              available in the cache and object retrieval failed then object
              with this state will also be looked-up in the cache and provided
              if available.
            - `timeout`: time interval after which retrieval of the object
              should be given up.
            - `freshness_period`: time interval after which the item created
              should become 'old'.
            - `expiration_period`: time interval after which the item created
              should become 'stale'.
            - `purge_period`: time interval after which the item created
              shuld be removed from the cache.
        :Types:
            - `object_class`: `classobj`
            - `address`: any hashable
            - `state`: "new", "fresh", "old" or "stale"
            - `object_handler`: callable(address, value, state)
            - `error_handler`: callable(address, error_data)
            - `timeout_handler`: callable(address)
            - `backup_state`: "new", "fresh", "old" or "stale"
            - `timeout`: `timedelta`
            - `freshness_period`: `timedelta`
            - `expiration_period`: `timedelta`
            - `purge_period`: `timedelta`
        """

        self._lock.acquire()
        try:
            if object_class not in self._caches:
                raise TypeError, "No cache for %r" % (object_class,)

            self._caches[object_class].request_object(address, state, object_handler,
                    error_handler, timeout_handler, backup_state, timeout,
                    freshness_period, expiration_period, purge_period)
        finally:
            self._lock.release()

    def tick(self):
        """Do the regular cache maintenance.

        Must be called from time to time for timeouts and cache old items
        purging to work."""
        self._lock.acquire()
        try:
            for cache in self._caches.values():
                cache.tick()
        finally:
            self._lock.release()

    def register_fetcher(self, object_class, fetcher_class):
        """Register a fetcher class for an object class.

        :Parameters:
            - `object_class`: class to be retrieved by the fetcher.
            - `fetcher_class`: the fetcher class.
        :Types:
            - `object_class`: `classobj`
            - `fetcher_class`: `CacheFetcher` based class
        """
        self._lock.acquire()
        try:
            cache = self._caches.get(object_class)
            if not cache:
                cache = Cache(self.max_items, self.default_freshness_period,
                        self.default_expiration_period, self.default_purge_period)
                self._caches[object_class] = cache
            cache.set_fetcher(fetcher_class)
        finally:
            self._lock.release()

    def unregister_fetcher(self, object_class):
        """Unregister a fetcher class for an object class.

        :Parameters:
            - `object_class`: class retrieved by the fetcher.
        :Types:
            - `object_class`: `classobj`
        """
        self._lock.acquire()
        try:
            cache = self._caches.get(object_class)
            if not cache:
                return
            cache.set_fetcher(None)
        finally:
            self._lock.release() def from_utf8(s):
    """
    Convert `s` to Unicode or leave unchanged if it is None.

    Regular strings are assumed to be UTF-8 encoded
    """
    if s is None:
        return None
    elif type(s) is unicode:
        return s
    elif type(s) is str:
        return unicode(s,"utf-8")
    else:
        return unicode(s) class IFeaturesProvider(IPyXMPPHelper):
    def get_features():
        """Return iterable of namespaces (features) supported, for disco#info
        query response."""
import logging
from pyxmpp.jabber.clientstream import LegacyClientStream
from pyxmpp.jabber.disco import DISCO_ITEMS_NS,DISCO_INFO_NS
from pyxmpp.jabber.disco import DiscoInfo,DiscoItems,DiscoIdentity
from pyxmpp.jabber import disco
from pyxmpp.client import Client
from pyxmpp.stanza import Stanza
from pyxmpp.cache import CacheSuite
from pyxmpp.utils import from_utf8
from pyxmpp.interfaces import IFeaturesProvider
# (C) Copyright 2003-2010 Jacek Konieczny <jajcus@jajcus.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License Version
# 2.1 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
"""Basic Jabber client functionality implementation.

Extends `pyxmpp.client` interface with legacy authentication
and basic Service Discovery handling.

Normative reference:
  - `JEP 78 <http://www.jabber.org/jeps/jep-0078.html>`__
  - `JEP 30 <http://www.jabber.org/jeps/jep-0030.html>`__
"""

__docformat__="restructuredtext en"



class JabberClient(Client):
    """Base class for a Jabber client.

    :Ivariables:
        - `disco_items`: default Disco#items reply for a query to an empty node.
        - `disco_info`: default Disco#info reply for a query to an empty node --
          provides information about the client and its supported fetures.
        - `disco_identity`: default identity of the default `disco_info`.
        - `register`: when `True` than registration will be started instead of authentication.
    :Types:
        - `disco_items`: `DiscoItems`
        - `disco_info`: `DiscoInfo`
        - `register`: `bool`
    """
    def __init__(self,jid=None, password=None, server=None, port=5222,
            auth_methods=("sasl:DIGEST-MD5","digest"),
            tls_settings=None, keepalive=0,
            disco_name=u"pyxmpp based Jabber client", disco_category=u"client",
            disco_type=u"pc"):
        """Initialize a JabberClient object.

        :Parameters:
            - `jid`: user full JID for the connection.
            - `password`: user password.
            - `server`: server to use. If not given then address will be derived form the JID.
            - `port`: port number to use. If not given then address will be derived form the JID.
            - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms
              in the list should be prefixed with "sasl:" string.
            - `tls_settings`: settings for StartTLS -- `TLSSettings` instance.
            - `keepalive`: keepalive output interval. 0 to disable.
            - `disco_name`: name of the client identity in the disco#info
              replies.
            - `disco_category`: category of the client identity in the disco#info
              replies. The default of u'client' should be the right choice in
              most cases.
            - `disco_type`: type of the client identity in the disco#info
              replies. Use `the types registered by Jabber Registrar <http://www.jabber.org/registrar/disco-categories.html>`__
        :Types:
            - `jid`: `pyxmpp.JID`
            - `password`: `unicode`
            - `server`: `unicode`
            - `port`: `int`
            - `auth_methods`: sequence of `str`
            - `tls_settings`: `pyxmpp.TLSSettings`
            - `keepalive`: `int`
            - `disco_name`: `unicode`
            - `disco_category`: `unicode`
            - `disco_type`: `unicode`
        """

        Client.__init__(self,jid,password,server,port,auth_methods,tls_settings,keepalive)
        self.stream_class = LegacyClientStream
        self.disco_items=DiscoItems()

        self.disco_info=DiscoInfo()